0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

浅析Linux内核数调用过程

冬至子 来源:Linux码农 作者:Linux码农 2022-11-16 16:25 次阅读

应用程序顺序调用接收数进行接收数数据时,内核数调用过程如下

sys_recv
 -> sys_recvfrom
    -> sock_recvmsg  
       -> __sock_recvmsg
          -> sock->ops->recvmsg => sock_common_recvmsg
             -> sk->sk_prot->recvmsg => tcp_recvmsg

最后协议栈通过调 使用tcp_recvmsg 从接收队列中获取数据采集到用户文件夹中。

图片

tcp_recvmsg

//把数据从接收队列中复制到用户空间中
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
        size_t len, int nonblock, int flags, int *addr_len)
{
    struct tcp_sock *tp = tcp_sk(sk);
    int copied = 0;
    u32 peek_seq;
    u32 *seq;
    unsigned long used;
    int err;
    int target;     /* Read at least this many bytes */
    long timeo;
    struct task_struct *user_recv = NULL;
    int copied_early = 0;
    //先对传输层上锁,以免在读的过程中,软中断操作传输层,对数据不同步造成后果
    lock_sock(sk);

    TCP_CHECK_TIMER(sk);
    //初始化错误码
    err = -ENOTCONN;
    //TCP_LISTEN状态 不允许读
    if (sk->sk_state == TCP_LISTEN)
        goto out;

     // 获取阻塞超时时间,若非阻塞读取,超时时间为0
    timeo = sock_rcvtimeo(sk, nonblock);

    /* Urgent data needs to be handled specially. */
    ///若读取外带数据,则跳转处理
    if (flags & MSG_OOB)
        goto recv_urg;

    /*判断是从缓冲区读取数据还是只是查看数据: 若是读取数据到用户空间,会更新copied_seq,
    而只是查看数据,不需更新copied_seq,所以在这里先判断是读缓冲区数据还是只是查看数据*/
    seq = &tp->copied_seq;
    if (flags & MSG_PEEK) {
        peek_seq = tp->copied_seq;
        seq = &peek_seq;
    }

    /* 根据是否设置MSG_WAITALL来确定本次调用需要接收数据的长度,
    若设置该标志,则读取数据的长度为用户调用时的输入参数len */
    target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);


    do {
        struct sk_buff *skb;
        u32 offset;

        /* Are we at urgent data? Stop if we have read anything or have SIGURG pending. 
           通过urg_data 和 urg_seq 来检测当前是否读取到外带数据。
        */
        if (tp->urg_data && tp->urg_seq == *seq) {
            //若在读取到外带数据之前已经读取了部分数据,则终止本次正常数据的读取。
            if (copied)
                break;
            //若用户进程有信号待处理,则也终止本次的读取
            if (signal_pending(current)) {
                copied = timeo ? sock_intr_errno(timeo) : -EAGAIN;
                break;
            }
        }

        /* Next get a buffer. */
        //获取下一个待读取的段
        skb = skb_peek(&sk->sk_receive_queue);
        do {
            //若队列为空,这只能接着处理prequeue或后备队列
            if (!skb)
                break;

            /* Now that we have two receive queues this
             * shouldn't happen.
             */
            //若接收队列中段序号大,说明也获取不到待读取的段,只能接着处理prequeue或后备队列
            if (before(*seq, TCP_SKB_CB(skb)->seq)) {
                printk(KERN_INFO "recvmsg bug: copied %X "
                       "seq %X\\n", *seq, TCP_SKB_CB(skb)->seq);
                break;
            }
            //计算该段读取数据的偏移位置,该偏移位置必须在该段的数据长度范围内才有效
            offset = *seq - TCP_SKB_CB(skb)->seq;
            ///SYN标志占用了一个序号,因此若存在SYN,则调整偏移
            if (skb->h.th->syn)
                offset--;
            //偏移位置必须在该段的数据长度范围内才有效
            if (offset < skb->len)
                goto found_ok_skb;
            //若存在fin标志,跳转处理
            if (skb->h.th->fin)
                goto found_fin_ok;
            BUG_TRAP(flags & MSG_PEEK);
            skb = skb->next;
        } while (skb != (struct sk_buff *)&sk->sk_receive_queue);

        /* Well, if we have backlog, try to process it now yet. */
        //只有在读取完数据后,才能在后备队列不为空的情况下,去处理接收到后备队列中的tcp段,否则终止本次读取
        if (copied >= target && !sk->sk_backlog.tail)
            break;
        /*接收队列中可读的段已读完,在处理prequeue或后备队列之前需要检测是否有导致返回的事件、状态等*/

        if (copied) {
            if (sk->sk_err || //有错误发送
                sk->sk_state == TCP_CLOSE ||
                (sk->sk_shutdown & RCV_SHUTDOWN) || //shutdown后不允许接收数据
                !timeo || //非阻塞
                signal_pending(current) || //收到信号
                (flags & MSG_PEEK)) //只是查看数据
                break; //上面检测条件只要有成立立即退出本次读取
        } else {
            //检测tcp会话是否即将终结
            if (sock_flag(sk, SOCK_DONE))
                break;
            ///有错误发生,返回错误码
            if (sk->sk_err) {
                copied = sock_error(sk);
                break;
            }

            if (sk->sk_shutdown & RCV_SHUTDOWN)
                break;
            //tcp状态处于close,而套接口不在终结状态,则进程可能是在读一个没有建立起连接的套接口,则返回ENOTCONN
            if (sk->sk_state == TCP_CLOSE) {
                if (!sock_flag(sk, SOCK_DONE)) {
                    /* This occurs when user tries to read
                     * from never connected socket.
                     */
                    copied = -ENOTCONN;
                    break;
                }
                break;
            }
            //未读到数据,且是非阻塞读,返回EAGAIN
            if (!timeo) {
                copied = -EAGAIN;
                break;
            }
            //检测是否收到数据,同时获取相应的错误码
            if (signal_pending(current)) {
                copied = sock_intr_errno(timeo);
                break;
            }
        }
        //检测是否有确认需要立即发送
        tcp_cleanup_rbuf(sk, copied);

        //在未启用sysctl_tcp_low_latency情况下,检查tcp_low_latency,默认其为0,表示使用prequeue队列
        if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {
            /* Install new reader */
            /*若是本次读取的第一此检测处理prequeue队列,则需要设置正在读取的进程描述符、缓存地址信息。这样当
            读取进程进入睡眠后,ESTABLISHED状态的接收处理就可能直接把数据复制到用户空间*/
            if (!user_recv && !(flags & (MSG_TRUNC | MSG_PEEK))) {
                user_recv = current;
                tp->ucopy.task = user_recv;
                tp->ucopy.iov = msg->msg_iov;
            }
            //更新当前可以使用的用户缓存大小
            tp->ucopy.len = len;

            BUG_TRAP(tp->copied_seq == tp->rcv_nxt ||
                 (flags & (MSG_PEEK | MSG_TRUNC)));


            //若prequeue不为空,跳转处理prequeue队列
            if (!skb_queue_empty(&tp->ucopy.prequeue))
                goto do_prequeue;

            /* __ Set realtime policy in scheduler __ */
        }
        //若数据读取完,调用release_sock 解锁传输控制块,主要用来处理后备队列
        if (copied >= target) {
            /* Do not sleep, just process backlog. */
            release_sock(sk);
            //锁定传输控制块,在调用lock_sock时进程可能会出现睡眠
            lock_sock(sk);
        } else
            /*若数据未读取,且是阻塞读取,则进入睡眠等待接收数据。在这种情况下,tcp_v4_do_rcv处理
              tcp段时可能会把数据直接复制到用户空间*/
            sk_wait_data(sk, &timeo);



        if (user_recv) {
            int chunk;

            /* __ Restore normal policy in scheduler __ */
            //更新剩余的用户空间长度和已复制到用户空间的数据长度
            if ((chunk = len - tp->ucopy.len) != 0) {
                NET_ADD_STATS_USER(LINUX_MIB_TCPDIRECTCOPYFROMBACKLOG, chunk);
                len -= chunk;
                copied += chunk;
            }
            /*若接收到接收队列中的数据已经全部复制到用户进程空间,但prequeue队列不为空,则需继续处理prequeue队列,
            并更新剩余的用户空间长度和已复制到用户空间的数据长度*/
            if (tp->rcv_nxt == tp->copied_seq &&
                !skb_queue_empty(&tp->ucopy.prequeue)) {
do_prequeue:
                tcp_prequeue_process(sk);

                if ((chunk = len - tp->ucopy.len) != 0) {
                    NET_ADD_STATS_USER(LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
                    len -= chunk;
                    copied += chunk;
                }
            }
        }
        //处理完prequeue队列后,若有更新copied_seq,且只是查看数据,则需要更新peek_seq
        if ((flags & MSG_PEEK) && peek_seq != tp->copied_seq) {
            if (net_ratelimit())
                printk(KERN_DEBUG "TCP(%s:%d): Application bug, race in MSG_PEEK.\\n",
                       current->comm, current->pid);
            peek_seq = tp->copied_seq;
        }
        //继续获取下一个待读取的段作处理
        continue;

    found_ok_skb:
        /* Ok so how much can we use? */
        /*获取该可读取段的数据长度,在前面的处理中已由tcp序号得到本次读取数据在该段中的偏移offset*/
        used = skb->len - offset;
        if (len < used)
            used = len;

        /* Do we have urgent data here? */
        //若段内包含带外数据,则获取带外数据在该段中的偏移
        if (tp->urg_data) {
            u32 urg_offset = tp->urg_seq - *seq;
            if (urg_offset < used) {
                //若偏移为0,说明目前需要的数据正是带外数据,且带外数据不允许进入正常的数据流
                if (!urg_offset) {
                    if (!sock_flag(sk, SOCK_URGINLINE)) {
                        ++*seq;
                        offset++;
                        used--;
                        if (!used)
                            goto skip_copy;
                    }
                } else
                    //若偏移不为0,则需要调整本次读取的正常数据长度直到读到带外数据为止
                    used = urg_offset;
            }
        }
        //处理读取数据的情况
        if (!(flags & MSG_TRUNC)) {
            {
                //将数据复制到用户空间
                err = skb_copy_datagram_iovec(skb, offset,
                        msg->msg_iov, used);
                if (err) {
                    /* Exception. Bailout! */
                    if (!copied)
                        copied = -EFAULT;
                    break;
                }
            }
        }

        *seq += used; //调整已读取数据的序号
        copied += used;//调整已读取数据的长度
        len -= used;//调整剩余的可用空间缓存大小

        //调整合理的tcp接收缓冲区大小
        tcp_rcv_space_adjust(sk);

skip_copy:
        //若对带外数据处理完毕,则将标志清零,
        if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) {
            tp->urg_data = 0;
            //设置首部标志,下一个接收段又可以通过首部预测执行快慢速路径
            tcp_fast_path_check(sk, tp);
        }
        //若该段还有数据未读取(如带外数据),则是能继续处理该段,而不能把该段从接收队列中删除
        if (used + offset < skb->len)
            continue;

        if (skb->h.th->fin)
            goto found_fin_ok;
        if (!(flags & MSG_PEEK)) {
            sk_eat_skb(sk, skb, copied_early);
            copied_early = 0;
        }
        //继续处理后续的段
        continue;

    found_fin_ok:
        /* Process the FIN. */
        //由于fin标志占用一个序号,因此当前读取的序号需递增
        ++*seq;
        if (!(flags & MSG_PEEK)) {
            sk_eat_skb(sk, skb, copied_early);
            copied_early = 0;
        }
        //接收到fin标志,无需继续处理后续的段
        break;
    } while (len > 0);

    if (user_recv) {
        //不空,处理prequeue
        if (!skb_queue_empty(&tp->ucopy.prequeue)) {
            int chunk;

            tp->ucopy.len = copied > 0 ? len : 0;

            tcp_prequeue_process(sk);
            //若在处理prequeue队列过程中又有一部分数据复制到用户空间,则调整剩余的可用空间缓存大小和已读数据的序号
            if (copied > 0 && (chunk = len - tp->ucopy.len) != 0) {
                NET_ADD_STATS_USER(LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
                len -= chunk;
                copied += chunk;
            }
        }

    /*清零,表示用户当前没有读取数据。这样当处理prequeue队列时不会将数据复制到用户空间,
        因为只有在未启用tcp_low_latency下,用户主动读取时,才有机会将数据直接复制到用户空间*/
        tp->ucopy.task = NULL;
        tp->ucopy.len = 0;
    }


    /*在完成读取数据后,需再次检测是否有必要立即发送ack,并根据情况确定是否发送ack段*/
    tcp_cleanup_rbuf(sk, copied);

    TCP_CHECK_TIMER(sk);
    //返回前解锁传输控制块
    release_sock(sk);
    //返回已读取的字节数
    return copied;

/*若在读取过程中发生了错误,则会跳转到此,解锁传输层后返回错误码*/
out:
    TCP_CHECK_TIMER(sk);
    release_sock(sk);
    return err;

/*若是接收带外数据,则调用tcp_recv_urg接收*/
recv_urg:
    err = tcp_recv_urg(sk, timeo, msg, len, flags, addr_len);
    goto out;
}

以上调使用排除带外数据只分析正常的数据的话,处理过程如下:

1、根据尚未从内核空间复制到用户空间的最前面一个字节的序号,找到待贝的数据块。

2、将数据从数据块中选择贝到用户空间。

3、调整合理的TCP接收绑定冲区大小

4、跳到第一步循环处理,直达到满足用户读取数量的条件。

审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • TCP
    TCP
    +关注

    关注

    8

    文章

    1274

    浏览量

    78334
  • LINUX内核
    +关注

    关注

    1

    文章

    311

    浏览量

    21393
  • ADD
    ADD
    +关注

    关注

    1

    文章

    20

    浏览量

    9301
收藏 人收藏

    评论

    相关推荐

    C语言函数调用过程中的内存变化解析

    相信很多编程新手村的同学们都会有一个疑问:C 语言如何调用函数的呢?局部变量的作用域为什么仅限于函数内?这个调用不是指C 语言上的函数调用的语法,而是在内存的视角下,函数的调用过程。本
    的头像 发表于 12-11 16:21 3473次阅读

    Linux内核的编译主要过程

    Linux内核的编译主要过程: 配置、编译、安装 。
    发表于 08-08 16:02 513次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>的编译主要<b class='flag-5'>过程</b>

    Linux内核中系统调用详解

    Linux内核中设置了一组用于实现各种系统功能的子程序,称为系统调用。用户可以通过系统调用命令在自己的应用程序中调用它们。从某种角度来看,系
    发表于 08-23 10:37 627次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>中系统<b class='flag-5'>调用</b>详解

    Linux内核自解压过程分析

    uboot完成系统引导以后,执行环境变量bootm中的命令;即,将Linux内核调入内存中并调用do_bootm函数启动内核,跳转至kernel的起始位置。
    的头像 发表于 12-08 14:00 412次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>自解压<b class='flag-5'>过程</b>分析

    Linux内核启动过程和Bootloader(总述)

    ,所以一般的 Bootloader 都会在执行过程中初始化一个串口做为调试端口(3)检测处理器类型 Bootloader在调用 Linux内核前必须检测系统的处理器类型,并将其保存到某
    发表于 08-18 17:35

    ARM linux系统调用的实现原理

    大家都知道linux的应用程序要想访问内核必须使用系统调用从而实现从usr模式转到svc模式。下面咱们看看它的实现过程
    发表于 05-30 11:24 2183次阅读

    Linux内核系统调用扩展研究

    系统凋用是操作系统内核提供给用户使用内核服务的接口。LinuX操作系统由于其自由开放性,用户可在原有基础上,添加新的系统调用,以便提供更多的服务。基于Linttx2.4
    发表于 07-25 16:09 40次下载
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>系统<b class='flag-5'>调用</b>扩展研究

    编译Linux2.6内核并添加一个系统调用

    本文以实例来详细描述了从准备一直到使用新内核Linux2.6 内核编译过程,然后介绍了添加系统调用的实现步骤,最后给实验结果。
    发表于 12-01 15:54 46次下载

    linux内核启动内核解压过程分析

    linux启动时内核解压过程分析,一份不错的文档,深入了解内核必备
    发表于 03-09 13:39 1次下载

    嵌入式系统内核引导启动过程浅析

    嵌入式系统内核引导启动过程浅析
    发表于 10-30 10:26 6次下载
    嵌入式系统<b class='flag-5'>内核</b>引导启动<b class='flag-5'>过程</b><b class='flag-5'>浅析</b>

    lattice DDR3 IP核的生成及调用过程

    本文以一个案例的形式来介绍lattice DDR3 IP核的生成及调用过程,同时介绍各个接口信号的功能作用
    发表于 03-16 14:14 1854次阅读
    lattice DDR3 IP核的生成及<b class='flag-5'>调用过程</b>

    如何区分xenomai、linux系统调用/服务

    对于同一个POSIX接口应用程序,可能既需要xenomai内核提供服务(xenomai 系统调用),又需要调用linux内核提供服务(
    的头像 发表于 05-10 10:28 1702次阅读

    Linux内核系统调用概述及实现原理

    本文介绍了系统调用的一些实现细节。首先分析了系统调用的意义,它们与库函数和应用程序接口(API)有怎样的关系。然后,我们考察了Linux内核如何实现系统
    的头像 发表于 05-14 14:11 1926次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>系统<b class='flag-5'>调用</b>概述及实现原理

    系统调用:用户栈与内核栈的切换(上)

    到 sysenter / sysexit 再到 syscall / sysret 实现方式的转变,关于具体的演化和区别、系统调用的其他细节等将在以后的系统调用专栏里分析。本文从系统调用最原始的int 0x80开始分析用户栈与
    的头像 发表于 07-31 11:27 610次阅读
    系统<b class='flag-5'>调用</b>:用户栈与<b class='flag-5'>内核</b>栈的切换(上)

    Linux系统调用的具体实现原理

    文我将基于 ARM 体系结构角度,从 Linux 应用层例子到内核系统调用函数的整个过程来梳理一遍,讲清楚linux系统
    的头像 发表于 09-05 17:16 765次阅读
    <b class='flag-5'>Linux</b>系统<b class='flag-5'>调用</b>的具体实现原理