本機發送IP資料包
本地傳送IP資料包是指資料包包括:傳輸層產生的資料包、裸IP、SCTP、IGMP,TCP和網路層的介面函式是ip_queue_xmit,UDP和網路層介面函式是ip_push_pending_frames,資料包對外發送在核心要做以下幾件事情。
a、查詢下一個站點
IP層需要知道完成資料包輸出功能的是那個網路裝置,以及下一個站點的路由資訊,尋找路由的任務由ip_route_output_flow函式完成。
b、初始化IP頭
初始化ip協議頭,處理IP選項、資料包分片、處理校驗和。
c、呼叫網路過濾子系統做安全檢查,不合法就丟掉
d、更新統計資訊
linux在傳輸層實現了多種不同傳輸層協議,不同的協議不同組織資料包不同,向網路層傳送資料包的方式也不同,如圖是傳輸層與網路層介面函式關係。
從上圖看從傳輸層到網路層,傳送資料包的方式主要分為兩種
1)、資料包傳給網路層時已經對資料包的分片做了大量的預處理,留給IP層的工作很多少了,比如(TCP、SCTP)。
2)、UDP和裸IP將資料包分片的工作都留給了IP層。
函式說明:
ip_queue_xmit:傳輸層TCP協議呼叫,將資料包從傳輸層傳送到網路層,建立協議頭和IP選項,然後呼叫dst_output傳送。
ip_append_data:傳輸層中UDP協議呼叫,快取傳輸層到網路層的請求傳送的資料包緩衝區。
ip_append_page:傳輸層UDP協議呼叫,快取傳輸層到網路層的請求傳送資料面。
ip_push_pending_frames:將ip_append_data和ip_append_page建立的資料包傳送佇列傳送出去。
dst_output:資料包傳送函式,當資料包的目標地址是其他主機初始化為ip_output。
ip_build_and_send_pkt:用於TCP傳送同步回答訊息時建立IP協議頭和選項併發送。
ip_send_reply:使用者TCP傳送回答訊息和復位是建立IP協議頭和選項併發送。
對於裸IP和IGMP他們自己構造IP協議頭,所以直接呼叫dst_output,TCP協議頭管理連線並向對端傳送回答訊息和復位訊息時呼叫ip_send_reply,ip_send_reply又呼叫ip_append_data和ip_push_pending_frames來回答訊息資料包。
一、關鍵資料結構體
1.1.struct sock
linux核心實現支援各種協議棧,struct sock接頭體是通用套接字資料結構,這個結構體資料很龐大,定義在include/net/sock.h中
struct sock {
/*
* Now struct inet_timewait_sock also uses sock_common, so please just
* don't add nothing before this first member (__sk_common) --acme
*/
//套接字在網路中最小描述,
//核心管理套接最重要的資訊結構體
struct sock_common __sk_common;
//定義sock_common元素的別名方便訪問
#define sk_node __sk_common.skc_node
#define sk_nulls_node __sk_common.skc_nulls_node
#define sk_refcnt __sk_common.skc_refcnt
#define sk_tx_queue_mapping __sk_common.skc_tx_queue_mapping
#define sk_copy_start __sk_common.skc_hash
#define sk_hash __sk_common.skc_hash
#define sk_family __sk_common.skc_family
#define sk_state __sk_common.skc_state
#define sk_reuse __sk_common.skc_reuse
#define sk_bound_dev_if __sk_common.skc_bound_dev_if
#define sk_bind_node __sk_common.skc_bind_node
#define sk_prot __sk_common.skc_prot
#define sk_net __sk_common.skc_net
kmemcheck_bitfield_begin(flags);
//tcp關閉套接字會發送RST資料包
...
}
1.2.struct inet_sock結構體
struct inet_sock繼承了通用套接字屬性(struct sock),是linux核心實現TCP/IP協議棧PF_INET協議族的套接字資料結構,struct inet_sock包含了傳送資料包大部分控制資訊:資料包目的ip、傳送網路裝置、ip選項。struct sock是struct inet_sock的第一個元素,所以他們的基礎地址是一樣的。
struct inet_sock {
/* sk and pinet6 has to be the first two members of inet_sock */
//通用套接字結構
struct sock sk;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
struct ipv6_pinfo *pinet6;
#endif
/* Socket demultiplex comparisons on incoming packets. */
//目標地址
__be32 inet_daddr;
//本機繫結的ip地址
__be32 inet_rcv_saddr;
//目的埠
__be16 inet_dport;
//本地應用程式建立套接字的埠號
__u16 inet_num;
//傳送資料包源地址
__be32 inet_saddr;
//資料包存活時間ttl
__s16 uc_ttl;
__u16 cmsg_flags;
//傳送資料包源埠
__be16 inet_sport;
//為分片資料包識別符號
__u16 inet_id;
//ip選項
struct ip_options *opt;
//資料包服務型別tos
__u8 tos;
__u8 min_ttl;
//組傳送存活時間ttl
__u8 mc_ttl;
//傳送路由最大mtu
__u8 pmtudisc;
__u8 recverr:1,
...
mc_all:1;
//組傳送網路裝置的索引號
int mc_index;
//組傳送地址
__be32 mc_addr;
struct ip_mc_socklist *mc_list;
struct {
//標誌位
unsigned int flags;
//產生的分片資料段大小
unsigned int fragsize;
//ip選項
struct ip_options *opt;
//路由表緩衝去入口
struct dst_entry *dst;
//ip資料包大小
int length; /* Total length of all frames */
//傳送資料包目標地址
__be32 addr;
//存放tcp兩端連線資訊
struct flowi fl;
} cork;
};
1.3、struct inet_sock struct sock關鍵函式
a、sk_dst_set 和__sk_dst_set函式
支援TCP協議的套機字,需要管理TCP連線,儲存目標地址使用的路由,
b、sk_dst_check和__sk_dst_check
檢查達到目標地址路由是否有效。
c、skb_set_owner_w
指定一個數據包所屬的套接字,也就是設定skb->sk資料域。
d、sock_alloc_send_skb和sock_wmalloc函式
分片sk_buff所需要的記憶體空間,sock_alloc_send_skb分配單個緩衝區或一系列分片資料包的第一個緩衝區,sock_wmalloc管理其餘的子分片。
二、ip_queue_ximit函式
ip_queue_xmit是傳輸層TCP協議和網路層的介面,ip_queue_xmit對資料包做一系列的IP層的初始化工作,本機產生的每個資料包都屬於某個套接字,包含套接字資料結構的指標skb->sk。ip_queue_ximit主要做以下工作
1、設定路由
先檢查skb->rtable是否為空,如果為空就要為資料包分配路由,如果沒有設定路由就呼叫ip_route_ouput_flow設定skb的路由,然後呼叫__sk_dst_check檢查目標路由是否可達。
...
rcu_read_lock();
//獲取路由
rt = skb_rtable(skb);
if (rt != NULL)
goto packet_routed;
/* Make sure we can route this packet. */
//檢查路由是否可達
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (rt == NULL) {
__be32 daddr;
/* Use correct destination address if we have options. */
//設定了源路由選項,重新查詢新路由和daddr取自源路由IP地址列表
daddr = inet->inet_daddr;
if(opt && opt->srr)
daddr = opt->faddr;
{
...
//路由不可達,重新選路由
if (ip_route_output_flow(sock_net(sk), &rt, &fl, sk, 0))
goto no_route;
}
//傳送資料包的輸出網路裝置資訊
sk_setup_caps(sk, &rt->u.dst);
}
...
2、構建IP協議頭
到此時skb只包含了IP資料包的常規負載資料、協議頭、從傳輸層傳來的負載資料。TCP為Socket Buffer分配記憶體是按找最大可能需要的來向系統申請記憶體,主要是考慮了下層協議頭需要空間,這樣做的可以避免IP層或更低協議在空間不夠的情況下做緩衝區複製或重分配提高效率。
...
/* OK, we know where to send it, allocate and build IP header. */
//檢視是否有足夠空間存放IP協議頭
skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
if (ip_dont_fragment(sk, &rt->u.dst) && !skb->local_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
//初始化IP協議頭的iph結構體各資料域
iph->ttl = ip_select_ttl(inet, &rt->u.dst);
//協議
iph->protocol = sk->sk_protocol;
//原地址
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
/* Transport layer set skb->h.foo itself. */
//構建IP選項
if (opt && opt->optlen) {
iph->ihl += opt->optlen >> 2;
ip_options_build(skb, opt, inet->inet_daddr, rt, 0);
}
//設定分片資料包的表示符ID
ip_select_ident_more(iph, &rt->u.dst, sk,
(skb_shinfo(skb)->gso_segs ?: 1) - 1);
//優先順序設定
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
...
3、函式結束處理
最後呼叫ip_local_out,再呼叫__ip_local_out,然後呼叫ip_send_check計算校驗和,最後呼叫網路過濾子系統OUT鏈上的鉤子處理函式,鉤子處理函式結束呼叫dst_output接續。
int __ip_local_out(struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph->tot_len = htons(skb->len);
//ip校驗和
ip_send_check(iph);
//呼叫OUT鏈上的鉤子處理函式
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
skb_dst(skb)->dev, dst_output);
}
以下是ip_queue_xmit完整程式碼:
int ip_queue_xmit(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
struct inet_sock *inet = inet_sk(sk);
struct ip_options *opt = inet->opt;
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();
//獲取路由
rt = skb_rtable(skb);
if (rt != NULL)
goto packet_routed;
/* Make sure we can route this packet. */
//檢查路由是否可達
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (rt == NULL) {
__be32 daddr;
/* Use correct destination address if we have options. */
//設定了源路由選項,重新查詢新路由和daddr取自源路由IP地址列表
daddr = inet->inet_daddr;
if(opt && opt->srr)
daddr = opt->faddr;
{
struct flowi fl = { .oif = sk->sk_bound_dev_if,
.mark = sk->sk_mark,
.nl_u = { .ip4_u =
{ .daddr = daddr,
.saddr = inet->inet_saddr,
.tos = RT_CONN_FLAGS(sk) } },
.proto = sk->sk_protocol,
.flags = inet_sk_flowi_flags(sk),
.uli_u = { .ports =
{ .sport = inet->inet_sport,
.dport = inet->inet_dport } } };
/* If this fails, retransmit mechanism of transport layer will
* keep trying until route appears or the connection times
* itself out.
*/
security_sk_classify_flow(sk, &fl);
//路由不可達,重新選路由
if (ip_route_output_flow(sock_net(sk), &rt, &fl, sk, 0))
goto no_route;
}
//傳送資料包的輸出網路裝置資訊
sk_setup_caps(sk, &rt->u.dst);
}
skb_dst_set_noref(skb, &rt->u.dst);
packet_routed:
if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
goto no_route;
/* OK, we know where to send it, allocate and build IP header. */
//檢視是否有足夠空間存放IP協議頭
skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
if (ip_dont_fragment(sk, &rt->u.dst) && !skb->local_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
//初始化IP協議頭的iph結構體各資料域
iph->ttl = ip_select_ttl(inet, &rt->u.dst);
//協議
iph->protocol = sk->sk_protocol;
//原地址
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
/* Transport layer set skb->h.foo itself. */
//構建IP選項
if (opt && opt->optlen) {
iph->ihl += opt->optlen >> 2;
ip_options_build(skb, opt, inet->inet_daddr, rt, 0);
}
//設定分片資料包的表示符ID
ip_select_ident_more(iph, &rt->u.dst, sk,
(skb_shinfo(skb)->gso_segs ?: 1) - 1);
//優先順序設定
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
res = ip_local_out(skb);
rcu_read_unlock();
return res;
no_route:
rcu_read_unlock();
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
kfree_skb(skb);
return -EHOSTUNREACH;
}