一、同步阻塞原理

如下简单代码

socket的创建

本次关注下图红色部分

其在内核态中的调用流程如下:

在整体流程中

  1. 系统调用接收请求,recvfrom

  2. 进入inet_stream_ops函数

  3. 进入tcp_recvmsg函数

  4. 判断接受队列中的数据是不是为空(服务端有无发送数据进来)

    1. 若没有发送数据进来,则系统内核修改当前进程状态为TASK_RUNNING,这时进程状态从可运行态陷入阻塞状态,然后把这个进程挂起来(依赖的数据还未就绪),最后主动让出CPU,linux将调度下一个进程

    2. 同时在进程睡眠之前,还会将进程添加到socket的等待队列之中,后面有数据到达,将进行唤醒

其对应代码如下图

 //file: net/core/sock.c
 int sk_wait_data(struct sock *sk, long *timeo)
 {
  //当前进程(current)关联到所定义的等待队列项上
  DEFINE_WAIT(wait);
  // 调⽤ sk_sleep 获取 sock 对象下的 wait
  // 并准备挂起,将进程状态设置为可打断 INTERRUPTIBLE
  prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
  set_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags);
  // 通过调⽤schedule_timeout让出CPU,然后进⾏睡眠
  rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue));
  ...

数据到达,内核唤醒

上述流程中,进程已经被阻塞,陷入睡眠状态,那么何时进程才会被唤醒呢?数据到达时。如何唤醒呢?具体流程如下:

  1. 首先数据包到达网卡进行接收

  2. 调用系统的接收函数tcp_v4_rcv,tcp_v4_do_rcv,tcp_rcv_established

  3. 然后调用tcp_queue_rcv函数将数据保存到socket的接受队列中,通过对应的hash算法进行映射到对应的具体socket上

  4. 现在接收队列已经有了数据,系统内核将会调用一个回调函数sk_data_ready(在创建 socket 流程⾥执⾏到的 sock_init_data 函数⾥已经把sk_data_ready 设置成 sock_def_readable 函数了。它是默认的数据就绪处理函数),从等待队列中将对应的进程寻找出来,找出来后,将其从阻塞状态变为TASK_INTERRUPTIPLE的就绪状态。内核在下一轮的调度算法中就有可能唤醒这个进程,运行到CPU上去

 static void sock_def_readable(struct sock *sk, int len)
 {
  struct socket_wq *wq;
  rcu_read_lock();
  wq = rcu_dereference(sk->sk_wq);
  //有进程在此 socket 的等待队列
  if (wq_has_sleeper(wq))
  //唤醒等待队列上的进程
  wake_up_interruptible_sync_poll(&wq->wait, POLLIN | POLLPRI |
  POLLRDNORM | POLLRDBAND);
  sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
  rcu_read_unlock();
 }

完整流程如下图

二、同步阻塞为什么性能差

进程间的切换太过频繁,而进程的切换是十分消耗资源的。

参考资料