【Linux内核】内核传输层协议处理过程

内容主要参考:《深入理解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 上等待而被阻塞的进程就被推入到可运行队列里了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值