浅析Linux内核数调用过程

应用程序顺序调用接收数进行接收数数据时,内核数调用过程如下
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 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 urg_data) { u32 urg_offset = tp->urg_seq - *seq; if (urg_offset 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 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、跳到第一步循环处理,直达到满足用户读取数量的条件。


华为将围绕着三个C助力运营商加速5G商用
抢高通饭碗,英特尔或将赢回iPhone订单?
IDEA 神器怎么修改不规范的代码
人工智能需要学习什么
华为起诉三星是迈向行业巅峰的敲门砖
浅析Linux内核数调用过程
实现快速高效设计的五个步骤
汽车点火线圈的工作原理及作用
雷布斯来踢馆,小米mix2发布会提前新iphone8发布会,颜值、性价比都不输阵
iphone12屏幕供应商:三星占大头,传无京东方
解决选择合适安全控制器的复杂性
国产耳机品牌Nank南卡重拳出击,发布骨传导耳机新标杆
苏州电感厂家科普0603贴片电感噪音产生的原因
手机处理器之外这3种配置也很重要
政策利好情况下 网络安全技术的划分会更加精细
[图文]Zone Alarm System
瑞萨电子推出为无人驾驶汽车运算SoC而打造的摄像头视频处理电路模块
基于RFID技术和MLX90121芯片实现电子巡更系统的设计
快递太多找不到?眼神不好有遗漏?看RFID电子标签如何渗透生活日常
自动驾驶传感器中如何现在激光雷达和毫米波雷达?