P.S. 阻塞分析从在调用过程中是否采用阻塞的判断逻辑看起就好。
socket的结构
Socket 使用tcp协议
-
协议使用NET_FAMILY中的AF_INET/PF_INET
-
内核是通过SOCK_STREAM来定位socket的,SOCK_STREAM结构如下:
[/net/ipv4/af_inst.c/tatic struct inet_protosw inetsw_array[]] static struct inet_protosw inetsw_array[] = { { .type = SOCK_STREAM, .protocol = IPPROTO_TCP, .prot = &tcp_prot, .ops = &inet_stream_ops, .flags = INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK, }, //其中还有UDP,Raw的部分,是通过结构体中的.opshe sk_prot等函数指针实现重载的 ... }
-
由上可知sock->ops = &inet_stream_ops,也就是说sock->ops->recvmsg = inet_recvmsg
[/net/ipv4/af_inet.c/struct proto_ops inet_stream_ops] const struct proto_ops inet_stream_ops = { .family = PF_INET, .owner = THIS_MODULE, ... .sendmsg = inet_sendmsg, .recvmsg = inet_recvmsg }
-
tcp_prot中的函数定义如下:其中.recvmsg 定义为tcp_recvmsg
[/net/ipv4/struct proto tcp_prot] 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, ... .recvmsg = tcp_recvmsg, ... }
阻塞与否的设置过程
-
阻塞的修改函数 fcntl。
-
实质:通过设置O_NONBLOCK标志位来控制Socket的阻塞与否。
-
位置:sock_fd对应的flip结构中的f_lags
-
过程:调用setfl函数进行设置,通过一开始那张图中的调用链修改flags
[/fs/fcntl.c/static int setfl(int fd, struct file * filp, unsigned long arg)] static int setfl(int fd, struct file * filp, unsigned long arg) { ... filp->f_flags = (arg & SETFL_MASK) | (filp->f_flags & ~SETFL_MASK); ... }
-
在调用过程中是否采用阻塞的判断逻辑
-
在调用socket_recv时,调用路径如下:
socket.recv ->sys_recv ->sys_recvfrom ->sock_recvmsg ->sock_recvmsg_nosec ->sock->ops->recvmsg
- 由上文可得:sock->ops->recvmsg即inet_recvmsg
-
在inet_recvmsg中, 第五个参数flags & MSG_DONTWAIT就是用来判断是否是需要阻塞的,这一步中的flags就是我们在setfl中设置的。
-
[/net/ipv4/af_inet.c/int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size, int flags)] int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size, int flags) { ... err = sk->sk_prot->recvmsg(sk, msg, size, flags & MSG_DONTWAIT, flags & ~MSG_DONTWAIT, &addr_len); ... }
-
在sk->sk_prot->recvmsg 中sk_prot=tcp_prot,所以最后调用的是tcp_prot->tcp_recvmsg。
-
-
tcp_recvmsg的函数签名如下:
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len)
- 如果在上文设置了O_NONBLOCK的话,设置给tcp_recvmsg的nonblock参数 > 0,即flags & MSG_DONTWAIT > 0. 关系如下图所示:(图片来自最后的参考资料)
-
tcp_recvmsg的函数签名如下:
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len){ ... // copied是指向用户空间拷贝了多少字节,即读了多少 int copied; ... // target指的是期望多少字节 int target; ... // 等效为timo = nonblock ? 0 : sk->sk_rcvtimeo; timeo = sock_rcvtimeo(sk, nonblock); ... // 如果设置了MSG_WAITALL标识target=需要读的长度 // 如果未设置,则为最低低水位值 target = sock_rcvlowat(sk, flags & MSG_WAITALL, len); ... do{ // 表明读到数据 if (copied) { // 注意,这边只要!timeo,即nonblock设置了就会跳出循环 if (sk->sk_err || sk->sk_state == TCP_CLOSE || (sk->sk_shutdown & RCV_SHUTDOWN) || !timeo || signal_pending(current) || (flags & MSG_PEEK)) break; }else{ // 到这里,表明没有读到任何数据 // 且nonblock设置了导致timeo=0,则返回-EAGAIN,符合我们的预期,即非阻塞方式 if (!timeo) { copied = -EAGAIN; break; } // 这边如果堵到了期望的数据,继续,否则当前进程阻塞在sk_wait_data上 if (copied >= target) { /* Do not sleep, just process backlog. */ //阻塞方式 release_sock(sk); lock_sock(sk); } else sk_wait_data(sk, &timeo); } while (len > 0); ...... return copied }
在上述代码中我们可以看出
- 如果copied>0, 则继续,否则的话,也就是没有读到想要的数据,[当设置了nonblock时,(表现在timeo上)],就返回-EAGAIN,也就是非阻塞方式。
- 但是如果没有设置nonblock,同时也没有出现copied >= target的情况,也就是没有读到想要的数据,则调用sk_wait_data将当前进程等待。也就是我们希望的阻塞方式。
- 文字表述无力,用别人的一张图吧,结合代码很容易懂
-
阻塞函数sk_wait_data所做的事情就是让出CPU。随后调用我在第三章被疯狂吐槽的schedule进行调度,进程切换的话也是第三章的那个switch_to,荡哥估计弄的差不多了,要是想了解的话可以去看看。
-
什么时候恢复运行呢?
- 网络数据来了
- 设定的超时时间到了
参考文献:
-
分析步骤参考:从linux源码看socket的阻塞和非阻塞
-
分析代码使用:openEuler