订阅
纠错
加入自媒体

Linux内核源代码:tcp/ip协议栈的调用

2021-06-21 10:33
一口Linux
关注

我们结合源码进行仔细分析:

接收端调用的是__sys_recvfrom函数:

__sys_recvfrom函数具体如下:

发现它调用了sock_recvmsg函数:

发现它调用了sock_recvmsg_nosec函数:

发现它调用了inet_recvmsg函数:

最后调用的是tcp_recvmsg这个系统调用。至此接收端调用分析完毕。

下面用gdb打断点进行验证:

验证结果刚好符合我们的调研。

4 传输层流程

4.1 发送端

传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,主要功能包括 (1)构造 TCP segment (2)计算 checksum (3)发送回复(ACK)包 (4)滑动窗口(sliding windown)等保证可靠性的操作。TCP 协议栈的大致处理过程如下图所示:

TCP 栈简要过程:

tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程。

构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer。

构造 TCP header。

计算 TCP 校验和(checksum)和 顺序号 (sequence number)。

TCP 校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。TCP校验和覆盖 TCP 首部和 TCP 数据。

TCP的校验和是必需的

发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。

UDP 栈简要过程:

UDP 将 message 封装成 UDP 数据报

调用 ip_append_data() 方法将 packet 送到 IP 层进行处理。

下面我们结合代码依次分析:

根据我们对应用层的追查可以发现,传输层也是先调用send()->sendto()->sys_sento->sock_sendmsg->sock_sendmsg_nosec,我们看下sock_sendmsg_nosec这个函数:

在应用层调用的是inet_sendmsg函数,在传输层根据后面的断点可以知道,调用的是sock->ops-sendmsg这个函数。而sendmsg为一个宏,调用的是tcp_sendmsg,如下;

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,
   .destroy        = tcp_v4_destroy_sock,
   .shutdown        = tcp_shutdown,
   .setsockopt        = tcp_setsockopt,
   .getsockopt        = tcp_getsockopt,
   .keepalive        = tcp_set_keepalive,
   .recvmsg        = tcp_recvmsg,
   .sendmsg        = tcp_sendmsg,
   ......

而tcp_sendmsg实际上调用的是

int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)

这个函数如下:

int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)

struct tcp_sock *tp = tcp_sk(sk);进行了强制类型转换
struct sk_buff *skb;
   flags = msg->msg_flags;
   ......
if (copied)
           tcp_push(sk, flags & ~MSG_MORE, mss_now,
                TCP_NAGLE_PUSH, size_goal);

在tcp_sendmsg_locked中,完成的是将所有的数据组织成发送队列,这个发送队列是struct sock结构中的一个域sk_write_queue,这个队列的每一个元素是一个skb,里面存放的就是待发送的数据。然后调用了tcp_push()函数。结构体struct sock如下:

struct sock{
   ...
struct sk_buff_head    sk_write_queue;指向skb队列的第一个元素
   ...
struct sk_buff    *sk_send_head;指向队列第一个还没有发送的元素

在tcp协议的头部有几个标志字段:URG、ACK、RSH、RST、SYN、FIN,tcp_push中会判断这个skb的元素是否需要push,如果需要就将tcp头部字段的push置一,置一的过程如下:

static void tcp_push(struct sock *sk, int flags, int mss_now,
int nonagle, int size_goal)

struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
   skb = tcp_write_queue_tail(sk);
if (!skb)
return;
if (!(flags & MSG_MORE) || forced_push(tp))
       tcp_mark_push(tp, skb);
   tcp_mark_urg(tp, flags);
if (tcp_should_autocork(sk, skb, size_goal)) {
avoid atomic op if TSQ_THROTTLED bit is already set
if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) {
           NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
           set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags);
       }
It is possible TX completion already happened
        * before we set TSQ_THROTTLED.
       
if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize)
return;
   }
if (flags & MSG_MORE)
       nonagle = TCP_NAGLE_CORK;
   __tcp_push_pending_frames(sk, mss_now, nonagle);

首先struct tcp_skb_cb结构体存放的就是tcp的头部,头部的控制位为tcp_flags,通过tcp_mark_push会将skb中的cb,也就是48个字节的数组,类型转换为struct tcp_skb_cb,这样位于skb的cb就成了tcp的头部。tcp_mark_push如下:

static inline void tcp_mark_push(struct tcp_sock *tp, struct sk_buff *skb)

   TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;
   tp->pushed_seq = tp->write_seq;

...
#define TCP_SKB_CB(__skb)    ((struct tcp_skb_cb *)&((__skb)->cb[0]))
...
struct sk_buff {
   ...    
char            cb[48] __aligned(8);
   ...struct tcp_skb_cb {
   __u32        seq;         Starting sequence number    
   __u32        end_seq;     SEQ + FIN + SYN + datalen    
   __u8        tcp_flags;     tcp头部标志,位于第13个字节tcp[13])    
   ......
};

然后,tcp_push调用了__tcp_push_pending_frames(sk, mss_now, nonagle);函数发送数据:

void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
int nonagle)

if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
              sk_gfp_mask(sk, GFP_ATOMIC)))
       tcp_check_probe_timer(sk);

发现它调用了tcp_write_xmit函数来发送数据:

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)

struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
unsigned int tso_segs, sent_pkts;
int cwnd_quota;
int result;
bool is_cwnd_limited = false, is_rwnd_limited = false;
   u32 max_segs;
统计已发送的报文总数
   sent_pkts = 0;
   ......
若发送队列未满,则准备发送报文
while ((skb = tcp_send_head(sk))) {
unsigned int limit;
if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
"skb_mstamp_ns" is used as a start point for the retransmit timer
           skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache;
           list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue);
           tcp_init_tso_segs(skb, mss_now);
goto repair;  Skip network transmission
       }
if (tcp_pacing_check(sk))
break;
       tso_segs = tcp_init_tso_segs(skb, mss_now);
       BUG_ON(!tso_segs);
检查发送窗口的大小
       cwnd_quota = tcp_cwnd_test(tp, skb);
if (!cwnd_quota) {
if (push_one == 2)
Force out a loss probe pkt.
               cwnd_quota = 1;
else
break;
       }
if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
           is_rwnd_limited = true;
break;
       ......
       limit = mss_now;
if (tso_segs > 1 && !tcp_urg_mode(tp))
           limit = tcp_mss_split_point(sk, skb, mss_now,
min_t(unsigned int,
                             cwnd_quota,
                             max_segs),
                           nonagle);
if (skb->len > limit &&
           unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE,
                     skb, limit, mss_now, gfp)))
break;
if (tcp_small_queue_check(sk, skb, 0))
break;
if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
break;
   ......

tcp_write_xmit位于tcpoutput.c中,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据,实际上调用的是__tcp_transmit_skb:

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
                 int clone_it, gfp_t gfp_mask, u32 rcv_nxt)

   skb_push(skb, tcp_header_size);
   skb_reset_transport_header(skb);
   ......
构建TCP头部和校验和
   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);
   tcp_options_write((__be32 *)(th + 1), tp, &opts);
   skb_shinfo(skb)->gso_type = sk->sk_gso_type;
if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
       th->window      = htons(tcp_select_window(sk));
       tcp_ecn_send(sk, skb, th, tcp_header_size);
   } else {
RFC1323: The window in SYN & SYN/ACK segments
        * is never scaled.
       
       th->window    = htons(min(tp->rcv_wnd, 65535U));
   }
   ......
   icsk->icsk_af_ops->send_check(sk, skb);
if (likely(tcb->tcp_flags & TCPHDR_ACK))
       tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt);
if (skb->len != tcp_header_size) {
       tcp_event_data_sent(tp, sk);
       tp->data_segs_out += tcp_skb_pcount(skb);
       tp->bytes_sent += skb->len - tcp_header_size;
   }
if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
       TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
                 tcp_skb_pcount(skb));
   tp->segs_out += tcp_skb_pcount(skb);
OK, its time to fill skb_shinfo(skb)->gso_{segs|size}
   skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);
   skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);
Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns)
Cleanup our debris for IP stacks
   memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),
                  sizeof(struct inet6_skb_parm)));
   err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
   ......

tcp_transmit_skb是tcp发送数据位于传输层的最后一步,这里首先对TCP数据段的头部进行了处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。

gdb调试验证如下:

4.2 接收端

传输层 TCP 处理入口在 tcp_v4_rcv 函数(位于 linux/net/ipv4/tcp ipv4.c 文件中),它会做 TCP header 检查等处理。

调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态。

如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。

对于传输层的代码阶段,我们需要分析recv函数,这个与send类似,调用的是__sys_recvfrom,整个函数的调用路径与send非常类似:

int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)

   ......
   err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
   sock = sockfd_lookup_light(fd, &err, &fput_needed);
   .....
   msg.msg_control = NULL;
   msg.msg_controllen = 0;
Save some cycles and don't copy the address if not needed
   msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
We assume all kernel code knows the size of sockaddr_storage
   msg.msg_namelen = 0;
   msg.msg_iocb = NULL;
   msg.msg_flags = 0;
if (sock->file->f_flags & O_NONBLOCK)
       flags |= MSG_DONTWAIT;
   err = sock_recvmsg(sock, &msg, flags);
if (err >= 0 && addr != NULL) {
       err2 = move_addr_to_user(&address,
                    msg.msg_namelen, addr, addr_len);
   .....

__sys_recvfrom调用了sock_recvmsg来接收数据,整个函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);,同样,根据tcp_prot结构的初始化,调用的其实是tcp_rcvmsg

接受函数比发送函数要复杂得多,因为数据接收不仅仅只是接收,tcp的三次握手也是在接收函数实现的,所以收到数据后要判断当前的状态,是否正在建立连接等,根据发来的信息考虑状态是否要改变,在这里,我们仅仅考虑在连接建立后数据的接收。

tcp_rcvmsg函数如下:

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)

......
if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) &&
(sk->sk_state == TCP_ESTABLISHED))
sk_busy_loop(sk, nonblock);
lock_sock(sk);
.....
if (unlikely(tp->repair)) {
err = -EPERM;
if (!(flags & MSG_PEEK))
goto out;
if (tp->repair_queue == TCP_SEND_QUEUE)
goto recv_sndq;
err = -EINVAL;
if (tp->repair_queue == TCP_NO_QUEUE)
goto out;
......
last = skb_peek_tail(&sk->sk_receive_queue);
skb_queue_walk(&sk->sk_receive_queue, skb) {
last = skb;
......
if (!(flags & MSG_TRUNC)) {
err = skb_copy_datagram_msg(skb, offset, msg, used);
if (err) {
Exception. Bailout!
if (!copied)
copied = -EFAULT;
break;


*seq += used;
copied += used;
len -= used;
tcp_rcv_space_adjust(sk);

<上一页  1  2  3  4  下一页>  
声明: 本文由入驻维科号的作者撰写,观点仅代表作者本人,不代表OFweek立场。如有侵权或其他问题,请联系举报。

发表评论

0条评论,0人参与

请输入评论内容...

请输入评论/评论长度6~500个字

您提交的评论过于频繁,请输入验证码继续

暂无评论

暂无评论

    人工智能 猎头职位 更多
    扫码关注公众号
    OFweek人工智能网
    获取更多精彩内容
    文章纠错
    x
    *文字标题:
    *纠错内容:
    联系邮箱:
    *验 证 码:

    粤公网安备 44030502002758号