linux_network_1

socket的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}

--> __sys_socket_create
--> sock_create
--> __sock_create
--> sock_alloc
--> // 获得每个协议族的操作表 rcu_dereference(net_families[family]);
--> pf->create 调用对应的协议栈创建函数
--> eg:AF_INET inet_create 对sock一些回调函数中=做绑定 对sock对象进行初始化
--> 绑定回调函数,当软中断收到数据包会调用sk_data_ready通知用户。

内核和用户进程之间阻塞

同步阻塞/recvfrom

1
2
3
4
5
6
同步阻塞的开销主要有:

进程通过recv系统调用接收一个socket上的数据时,如果数据没有到达,进程就被从CPU上拿下来,然后再换上另一个进程。这导致一次进程上下文切换的开销。
当连接上的数据就绪的时候,睡眠的进程又会被唤醒,又是一次进程切换的开销。
一个进程同时只能等待一条连接,如果有很多并发,则需要很多进程,每个进程都将占用几MB的内存。
从CPU开销角度来看,一次同步阻塞网络IO将导致两次进程上下文切换开销,每一次大约3-5us。另外一个进程同一时间只能处理一个socket。太差。

多路复用epoll

1
2
3
4
5
epoll_create
epoll_ctl
epoll_wait


1
2
3
4
本质上是 极大程序的减少了无用的进程上下文切换,让进程更专注处理网络请求。
在内核的硬,软
中断上下文中,包从网卡接收过来进行处理,然后放到socket的接收队列,再找到socket关联的epitem,并加入到epoll对象的就绪链表中。
在用户进程中,通过调用epoll wait来查看就绪链表中是否有事件到达,如果有,直接取走做处理。处理完毕再次调用epoll wait。至于红黑树,仅仅是提高了epoll查找,添加,删除socket时的效率而已。不算根本原因。
1
epoll也是阻塞的。没有请求处理的时候,也会让出CPU。阻塞不会导致低性能。过多的阻塞才会。W

阻塞 非阻塞

阻塞指的是进程因为等待某个事件而主动让出CPU挂起的操作。在网络IO中。当进程等待socket上的数据时,如果数据还没有到来,那就把当前状态从TASK_RUNNING修改为TASK_INTERRUPTIBLE。然后主动让出CPU,由调度器来调度下一个就绪状态的进程来执行。

所以,以后分析是否阻塞就看是否让出CPU。

内核是如何发送网络包的?

网络包发送过程总览

用户进程在用户态通过send系统调用接口进入内核态,send通过内存拷贝skb,进程协议处理进入驱动ring buffer,通过pci总线发送后,产生中断通知发送完成,并清理ring buffer。

1
2
3
4
5
6
7
8
9
10
send 
--> SYSCALL_DEFINE6 sendto
--> sock_sendmsg 系统调用
--> sock->ops->sendmsg
--> sk->sk_prot->sendmsg 协议栈
--> tcp_send_msg- 封装tcp头,调用网络层发送接口 queue_xmit 传输层
--> ip_local_out(skb) 网络层
--> dst_ngigh_output
--> dev_queue_xmit(skb) 调用网卡驱动中的发送回调函数 将数据包传递给网卡设备 网络设备子系统--> 硬件

数据发送完毕后,释放缓存 队列等内存。在网卡发送完毕后,给CPU发送一个硬中断来通知cpu。实际是触发了 NET_RX_SOFTIRQ 。所以服务器中proc softirqs里面NET_TX要比RX大很多。

网卡启动准备

网卡启动最重要的任务之一就是分配和初始化RingBuffer。在对应的驱动程序的open函数中,对于ringbuffer进行分配。

1
2
3
igb_setup_all_tx_resources
igb_setup_all_rx_resources
netif_tx_start_all_queues 开启全部队列

为什么要用环形队列?好处是什么

特性/对比点 环形队列(Ring Buffer) 普通 FIFO 队列(如链表)
内存分配方式 预分配,连续内存块 动态分配,每个节点独立分配
缓存命中率 高(连续内存 + 局部性好) 低(指针跳转 + 内存分散)
指针操作复杂度 简单,仅需要 head 和 tail 操作复杂,涉及节点申请/释放
支持无锁操作 易于 lockless 实现,尤其单生产者/单消费者模型 难,容易涉及竞态和锁
空间使用效率 高,数组固定大小,空间紧凑 较低,节点指针额外开销
硬件 DMA 支持 很多硬件直接支持环形 DMA 描述符结构 不适用于 DMA 映射
数据结构大小固定性 是,数组固定大小,易于调优和估算 否,链表大小动态变化,管理麻烦
实现难度 结构简单,逻辑清晰 相对复杂,需要考虑链表指针等各种异常处理

数据从用户进程到网卡的过程

send系统调用实现

image-20250630084205169

sendto中 构造 找到sock,构造msg后 通过 __sock_sendmsg 发送,到sock_sendmsg_nosec中,通过 inet6_sendmsg 调用进入协议栈。

传输层处理

在进入协议栈后,会找到具体的发送函数。对tcp来说是 tcp_sendmsg –> tcp_sendmsg_locked。 tcp_write_queue_tail获取发送队列的最后一个skb。把用户内存里的数据拷贝到内核态,涉及到1次/几次内存拷贝的开销。

1
2
3
4
5
if (forced_push(tp)) {
tcp_mark_push(tp, skb);
__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
} else if (skb == tcp_send_head(sk))
tcp_push_one(sk, mss_now);

调用tcp_push_one / __tcp_push_pending_frames 将数据包发送出去。

最终会调用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Send _single_ s






kb sitting at the send head. This function requires
* true push pending frames to setup probe timer etc.
*/
void tcp_push_one(struct sock *sk, unsigned int mss_now)
{
struct sk_buff *skb = tcp_send_head(sk);

BUG_ON(!skb || skb->len < mss_now);

tcp_write_xmit(sk, mss_now, TCP_NAGLE_PUSH, 1, sk->sk_allocation);
}

网络层发送处理

tcp_write_xmit内部处理了传输层的拥塞控制,滑动窗口相关的工作,满足窗口要求的时候,设置TCP头然后将skb传到更低的网络层进行处理。

tcp_transmit_skb开启真正的发送函数。clone新的tcp出来,封装tcp的头.

1
2
3
4
5
6
7
8
9
/* Build TCP header and checksum it. */
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);
*(((__be16 *)th) + 6) = htons(((tcp_header_.....

err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);调用网络层发送接口.

为什么要进行clone? tcp支持超时重传, 在收到对方的ACK之前,这个skb’不能被删除.等收到ACK后再次删除.

tcp_options_write中对TCP头进行设置, skb中包含了网络协议中的所有头,在设置TCP的头的时候,只需要把指针指向skb的合适位置,后面设置IP头的时候,指针在挪一挪即可.避免频繁的内存申请拷贝.

1
2
3
4
5
6
7
8
9
10
11
queue_xmit 在 ipv4中 实际值得是.
.queue_xmit = ip_queue_xmit 进入 网络层
主要处理路由表项的查找,IP头设置,netfilter过滤,大于MTU时的skb切片,

__ip_local_out执行netfilter过滤, 如果使用iptables设置了一些规则,那么这里将检测是否命中,如果设置的规则非常复杂,那么这里将导致CPU开销大增.
ip_finish_output2
ip_finish_output中,对大于MTU的包进行分片后调用 ip_finish_output2

根据下一条IP地址查找邻居项,找不到就创建一个.继续向邻居子系统传递.
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
neigh_output

邻居子系统

位于网络层和数据链路层中间的一个系统, 为网络层提供一个下层的封装, 让网络层不必关心下层的地址信息, 让下层决定发送到哪个MAC地址. 主要查找/创建邻居项,在创建邻居项的时候,可能会发出实际的ARP请求,然后封装MAC头,将发送过程传递到更下层的网络设备子系统.

调用 neigh_resolve_output 发出,有可能触发arp请求.

最后调用 dev_queue_xmit 将skb传递给Linux网络设备子系统

linux网络设备子系统

__dev_queue_xmit()
└── __dev_queue_xmit_xmit()
└── dev_hard_start_xmit()
└── xmit_one()
└── dev->netdev_ops->ndo_start_xmit()

总览

TX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// 1.分配 sk_buff,复制用户数据
// net/ipv4/ip_output.c
struct sk_buff *ip_make_skb(struct sock *sk,
struct flowi4 *fl4,
int getfrag(void *from, char *to, int offset,
int len, int odd, struct sk_buff *skb),
void *from, int length, int transhdrlen,
struct ipcm_cookie *ipc, struct rtable **rtp,
struct inet_cork *cork, unsigned int flags)
{
// 管理 sk_buff 双链表
struct sk_buff_head queue;
__skb_queue_head_init(&queue);
// ...
// NOTE: 具体分配sk_buffer的地方
err = __ip_append_data(sk, fl4, &queue, cork,
&current->task_frag, getfrag,
from, length, transhdrlen, flags);
// 给IP协议头赋值
return __ip_make_skb(sk, fl4, &queue, cork);
}

// 具体分配sk_buffer
static int __ip_append_data(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork,
struct page_frag *pfrag,
int getfrag(void *from, char *to, int offset,
int len, int odd, struct sk_buff *skb),
void *from, int length, int transhdrlen,
unsigned int flags)
{
// 拿到队列尾部的skb
skb = skb_peek_tail(queue);
// 分配 sk_buff
if (!skb)
goto alloc_new_skb;
//...
alloc_new_skb:
//...
// 分配新 sk_buff
skb = sock_alloc_send_skb(sk, alloclen,
(flags & MSG_DONTWAIT), &err);
//...
// ip 协议头长度
fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);
//...
// sk_buff 放入数据的地方,设置了网络层头、传输层头
// L3层链路层的协议头,在后续处理
data = skb_put(skb, fraglen + exthdrlen - pagedlen);
skb_set_network_header(skb, exthdrlen);
skb->transport_header = (skb->network_header +
fragheaderlen);
// ...
// 从 from 复制copy数量的数据到 data+transhdrlen
// transhdrlen 是在调用ip_make_skb是计算的udp协议头大小
// 也就是说把用户数据复制到udp数据报的数据部分
if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) {
// ...
// NOTE: 将 sk_buff 放入队列
__skb_queue_tail(queue, skb);
// ...
}

// 设置ip协议头
struct sk_buff *__ip_make_skb(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork)
{
// ...
skb = __skb_dequeue(queue); // NOTE: 从队列中获取一个skb

//...
// NOTE: 设置 IP协议头:版本、源IP地址、目标IP地址
iph = ip_hdr(skb);
iph->version = 4;
//...
ip_copy_addrs(iph, fl4); // NOTE: 设置ip地址
//...

}

// 2. 给协议头复制,发往下一层
// net/ipv4/udp.c
// NOTE: 这里获取了skb 的 UDP 协议头,进行赋值
static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4,
struct inet_cork *cork)
{

struct udphdr *uh;
uh = udp_hdr(skb);
// NOTE 设置udp协议头:源端口、目标端口
uh->source = inet->inet_sport;
uh->dest = fl4->fl4_dport;

// 往下一层发送 skb
err = ip_send_skb(sock_net(sk), skb);
}

// include/net/neighbour.h
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb,
bool skip_cache)
{
// ...
return neigh_hh_output(hh, skb); // NOTE: 使用缓存的硬件地址发送,也就是mac地址,hh里包含协议头
// ...
}
static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
{
// ...
hh_len = READ_ONCE(hh->hh_len); // NOTE: 获取头长度
memcpy(skb->data - HH_DATA_MOD, hh->hh_data,
HH_DATA_MOD); // NOTE: skb->data 指针上移,放置 hh->hh_data 协议头
// ...
return dev_queue_xmit(skb); // NOTE: 发往网络设备子系统处理
}


RX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
1. 软中断
// net/core/dev.c
__napi_poll(n->poll,网卡初始化时注册的igb_poll)
// drivers/net/ethernet/intel/igb/igb_main.c
igb_poll -> igb_clean_rx_irq -> napi_gro_receive(skb 结构体已包含可用的 L2 数据)

2. gro: Generic Receive Offloading
// net/core/gro.c
napi_gro_receive -> napi_skb_finish -> gro_normal_one
// include/net/gro.h
gro_normal_one -> gro_normal_list -> netif_receive_skb_list_internal

// net/core/dev.c
netif_receive_skb_list_internal -> __netif_receive_skb_list -> __netif_receive_skb_list_core -> __netif_receive_skb_core
-> deliver_ptype_list_skb -> deliver_skb(pt_prev->func指向ip_rcv,通过dev_add_pack注册ip_packet_type)

// NOTE: 再进行Linux网络栈处理

// 1. IP 网络层
// net/ipv4/ip_input.c
ip_rcv -> ip_rcv_finish -> dst_input
// 1.1 路由
// include/net/dst.h
dst_input -> ip_local_deliver
// net/ipv4/ip_input.c
ip_local_deliver -> ip_local_deliver_finish -> ip_protocol_deliver_rcu -> udp_rcv

// 2. 传输层
// net/ipv4/udp.c
udp_rcv -> __udp4_lib_rcv -> udp_unicast_rcv_skb -> udp_queue_rcv_skb -> udp_queue_rcv_one_skb -> __udp_queue_rcv_skb
-> __udp_enqueue_schedule_skb -k __skb_queue_tail -> sk_data_ready/sock_def_readable

// 3. 用户/系统调用层
// net/core/sock.c
sock_def_readable -> sk_wake_async_rcu
// include/net/sock.h
sk_wake_async_rcu -> sock_wake_async


参考资料

https://blog.csdn.net/weixin_44260459/article/details/121480230

image-20250724115436779

tcpdump抓包?

image-20250724115825198

主要通过libpcap库实现,内部通过BPF技术实现数据过滤。将用户定义的过滤条件以字节码的形式注入内核, 由内核层筛选网络包后,将满足条件的包交给用户态处理。

tcpdump通过 AF_PACKET socket 接收以太网帧;工作在链路层。

image-20250724122117449