内容主要参考:《深入理解Linux网络》,公众号「开发内功修炼」
一,UDP
UDP协议基于IP层,它没有自己的状态机,仅仅是IP层上做了一些封装,所有行为和IP协议一样。但是多了一个port概念,指从内核角度看到的应用程序标识。
1,udp处理
udp协议的处理函数是 udp_rcv,它跳转到__udp4_lib_rcv(),此函数主要用于验证数据包,分析归类其丢包原因,包括:无socket、目标端口错误、数据包过小、数据包损坏。
//file: net/ipv4/udp.c
int udp_rcv(struct sk_buff *skb)
{
return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}
int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
int proto)
{
//根据 skb 来寻找对应的socket,当找到以后将数据包放到socket 的缓存队列⾥。
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
if (sk)
return udp_unicast_rcv_skb(sk, skb, uh);
...
/* No socket. Drop packet silently, if checksum is wrong */
if (udp_lib_checksum_complete(skb))
goto csum_error;
//如果没有找到socket,则判断为无socket类型丢包,同时发送⼀个⽬标不可达的 icmp 包
drop_reason = SKB_DROP_REASON_NO_SOCKET;
__UDP_INC_STATS(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
}
__udp4_lib_lookup_skb 是根据 skb 来寻找对应的socket,当找到以后将数据包放到socket 的缓存队列里。如果没有找到,则发送⼀个目标不可达的 icmp 包。
函数执行过程:__udp4_lib_rcv—> udp_unicast_rcv_skb —> udp_queue_rcv_skb
//file: net/ipv4/udp.c
static int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
struct sk_buff *next, *segs;
int ret;
if (likely(!udp_unexpected_gso(sk, skb)))
return udp_queue_rcv_one_skb(sk, skb);//检验,>0重新找协议,-1错误,0正确
BUILD_BUG_ON(sizeof(struct udp_skb_cb) > SKB_GSO_CB_OFFSET);
__skb_push(skb, -skb_mac_offset(skb));
segs = udp_rcv_segment(sk, skb, true);
skb_list_walk_safe(segs, skb, next) {
__skb_pull(skb, skb_transport_offset(skb));//处理传输偏移
udp_post_segment_fix_csum(skb);
ret = udp_queue_rcv_one_skb(sk, skb);
if (ret > 0)
ip_protocol_deliver_rcu(dev_net(skb->dev), skb, ret);//重找协议
}
return 0;
}
关键函数udp_queue_rcv_one_skb(),首先将skb放入套接字(执行函数待查),如果队列已满则丢弃,后续检查协议,返回值>0重新找协议,-1错误,0正确。对接收缓冲区进行更新,将数据包插入接收队列尾部,最后通知等待读取数据而被置于睡眠的进程进行数据包读取。
2,用户进程处理
Linux 内核对数据包的接收和处理过程,最后把数据包放到 socket 的接收队列中了。在代码里调用的 recvfrom 是⼀个 glibc 的库函数,该函数在执行后会将用户进行陷入到内核态,进入到 Linux 实现的系统调用 sys_recvfrom 。
socket 数据结构中的 const struct proto_ops 对应的是协议的⽅法集合。每个协议都会实现不同的方法集,对于 udp 来说,是通过 inet_dgram_ops 来定义的,其中注册了 inet_recvmsg 方法。
struct sock *sk 是⼀个非常重要的子结构体。其中的 sk_prot 定义了⼆级处理函数。对于udp协议来说,会被设置成 udp 协议实现的方法集 udp_prot 。
//net/core/datagram.c
struct sk_buff *skb_recv_datagram(struct sock *sk, unsigned int flags,
int *err)
{
int off = 0;
return __skb_recv_datagram(sk, &sk->sk_receive_queue, flags,
&off, err);
}
所以可以看到,用户进程在内核态的读取过程,就是访问sk->sk_receive_queue
二,TCP
1,socket创建
//file:net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}
系统调用__sys_socket->sock_create->__sock_create
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
...
//分配sock对象
sock = sock_alloc();
//获取协议族操作表
pf = rcu_dereference(net_families[family]);
//调⽤其 create ⽅法。 对于 AF_INET 协议族来说,执⾏到的是inet_create ⽅法。
err = pf->create(net, sock, protocol, kern);
}
协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合。
//file:net/ipv4/af_inet.c
tatic int inet_create(struct net *net, struct socket *sock, int
protocol,
int kern)
{
struct sock *sk;
//查找对应的协议,对于TCP SOCK_STREAM 就是获取到了
//static struct inet_protosw inetsw_array[] =
//{
// {
// .type = SOCK_STREAM,
// .protocol = IPPROTO_TCP,
// .prot = &tcp_prot,
// .ops = &inet_stream_ops,
// .no_check = 0,
// .flags = INET_PROTOSW_PERMANENT |
// INET_PROTOSW_ICSK,
// },
//}
list_for_each_entry_rcu(answer, &inetsw[sock->type], list)
{
//将 inet_stream_ops 赋到 socket->ops 上
sock->ops = answer->ops;
//获得 tcp_prot
answer_prot = answer->prot;
//分配 sock 对象, 并把 tcp_prot 赋到 sock->sk_prot 上
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
//对 sock 对象进⾏初始化
sock_init_data(sock, sk);
}
在 inet_create 中,根据类型 SOCK_STREAM 查找到对于 tcp 定义的操作方法实现集合inet_stream_ops 和 tcp_prot。并把它们分别设置到 socket->ops 和 sock->sk_prot 上。
sock_init_data将 sock 中的 sk_data_ready 函数指针进行了初始化,设置为默认 sock_def_readable()
//file: net/core/sock.c
void sock_init_data(struct socket *sock, struct sock *sk)
{
sk->sk_state_change = sock_def_wakeup;
sk->sk_data_ready = sock_def_readable;
sk->sk_write_space = sock_def_write_space;
sk->sk_error_report = sock_def_error_report;
sk->sk_destruct = sock_def_destruct;
...
}
当软中断上收到数据包时会通过调用 sk_data_ready 函数指针 来唤醒在 sock 上等待的进程。
至此,⼀个 tcp对象,确切地说是 AF_INET 协议族下 SOCK_STREAM对象就算是创建完成。
2,等待接收数据
进入 recvfrom 系统调⽤后,⽤户进程就进入到了内核态,通过执行⼀系列的内核协议层函数,然后到socket 对象的接收队列中查看是否有数据,没有的话就把自己添加到 socket 对应的等待队列里。最后让出CPU,操作系统会选择下⼀个就绪状态的进程来执行。
//file: net/ipv4/tcp.c
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
do {
skb_queue_walk(&sk->sk_receive_queue, skb) {
...
}
if (copied >= target) {
release_sock(sk);
lock_sock(sk);
} else {//没有收到⾜够数据,启⽤ sk_wait_data 阻塞当前进程
sk_wait_data(sk, &timeo, last);
}
...
}
使用skb_queue_walk 遍历接收队列接收数据。sk_wait_data等待队列处理。
int sk_wait_data(struct sock *sk, long *timeo, const struct sk_buff *skb)
{
DEFINE_WAIT_FUNC(wait, woken_wake_function);
int rc;
add_wait_queue(sk_sleep(sk), &wait);
sk_set_bit(SOCKWQ_ASYNC_WAITDATA, sk);
rc = sk_wait_event(sk, timeo, skb_peek_tail(&sk->sk_receive_queue) != skb, &wait);
sk_clear_bit(SOCKWQ_ASYNC_WAITDATA, sk);
remove_wait_queue(sk_sleep(sk), &wait);
return rc;
}
sk_sleep 获取 sock 对象下的等待队列列表头wait_queue_head_t;
add_wait_queue 把新定义的等待队列项 wait 插⼊到 sock 对象的等待队列下;
sk_wait_event 让出 CPU ,进程将进⼊睡眠状态,等待软中断唤醒。
3,软中断
软中断 ksoftirqd 进程,收到数据包以后:
- 若是 tcp 的包 执行 tcp_v4_rcv 函数;
- 若是 ESTABLISH 状态下的数据包,把数据拆出来放到对应 socket 的接收队列中;
最后调用 sk_data_ready 来唤醒用户进程。
// file: net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);//获取ip header
lookup://根据数据包 header 中的 ip、端⼝信息查找到对应的socket
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
//用户未被锁定
if (!sock_owned_by_user(sk)) {
skb_to_free = sk->sk_rx_skb_cache;
sk->sk_rx_skb_cache = NULL;
//接收数据
ret = tcp_v4_do_rcv(sk, skb);
}
...
}
在 tcp_v4_rcv 中首先根据收到的网络包的 header 里的 source 和 dest 信息来在本机上查询对应的 socket。找到以后,执行函数 tcp_v4_do_rcv 。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
//ESTABLISH 状态的数据包处理
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst = sk->sk_rx_dst;
...
tcp_rcv_established(sk, skb);
return 0;
}
//⾮ ESTABLISH 状态的数据包处理
}
//file: net/ipv4/tcp_input.c
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
......
//接收数据到队列中
eaten = tcp_queue_rcv(sk, skb, tcp_header_len,
&fragstolen);
//数据 ready,唤醒 socket 上阻塞掉的进程
sk->sk_data_ready(sk, 0);
在 tcp_rcv_established 中通过调用 tcp_queue_rcv 函数中完成了将接收数据放到 socket的接收队列上。
//file: net/ipv4/tcp_input.c
static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb,
bool *fragstolen)
{
...
if (!eaten) {
//把收到的数据放到socket的接收队列尾部
__skb_queue_tail(&sk->sk_receive_queue, skb);
skb_set_owner_r(skb, sk);
}
}
void tcp_data_ready(struct sock *sk)
{
...
//软中断唤醒
sk->sk_data_ready(sk);
}
//file: net/ipv4/tcp_input.c
static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb,
bool *fragstolen)
{...
if (!eaten) {
__skb_queue_tail(&sk->sk_receive_queue, skb);
skb_set_owner_r(skb, sk);
}
}
void tcp_data_ready(struct sock *sk)
{
...
//软中断唤醒
sk->sk_data_ready(sk);
}
在 socket 上等待而被阻塞的进程就被推入到可运行队列里了。