Socket阻塞和非阻塞代码分析 流程向

P.S. 阻塞分析从在调用过程中是否采用阻塞的判断逻辑看起就好。

socket的结构

Socket阻塞和非阻塞代码分析 流程向_第1张图片

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);
      	...
      }
      

在调用过程中是否采用阻塞的判断逻辑

  1. 在调用socket_recv时,调用路径如下:

    socket.recv
    			->sys_recv
    						->sys_recvfrom
    									->sock_recvmsg
    												->sock_recvmsg_nosec
      														->sock->ops->recvmsg
    
    • 由上文可得:sock->ops->recvmsg即inet_recvmsg
  2. 在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。

  3. 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. 关系如下图所示:(图片来自最后的参考资料)Socket阻塞和非阻塞代码分析 流程向_第2张图片
  4. 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将当前进程等待。也就是我们希望的阻塞方式。
    • 文字表述无力,用别人的一张图吧,结合代码很容易懂Socket阻塞和非阻塞代码分析 流程向_第3张图片
  5. 阻塞函数sk_wait_data所做的事情就是让出CPU。随后调用我在第三章被疯狂吐槽的schedule进行调度,进程切换的话也是第三章的那个switch_to,荡哥估计弄的差不多了,要是想了解的话可以去看看。

  6. 什么时候恢复运行呢?

    • 网络数据来了
    • 设定的超时时间到了

参考文献:

  1. 分析步骤参考:从linux源码看socket的阻塞和非阻塞

  2. 分析代码使用:openEuler

你可能感兴趣的:(openEuler)