一、同步阻塞原理
如下简单代码
socket的创建
本次关注下图红色部分
其在内核态中的调用流程如下:
在整体流程中
系统调用接收请求,recvfrom
进入inet_stream_ops函数
进入tcp_recvmsg函数
判断接受队列中的数据是不是为空(服务端有无发送数据进来)
若没有发送数据进来,则系统内核修改当前进程状态为TASK_RUNNING,这时进程状态从可运行态陷入阻塞状态,然后把这个进程挂起来(依赖的数据还未就绪),最后主动让出CPU,linux将调度下一个进程
同时在进程睡眠之前,还会将进程添加到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));
...
数据到达,内核唤醒
上述流程中,进程已经被阻塞,陷入睡眠状态,那么何时进程才会被唤醒呢?数据到达时。如何唤醒呢?具体流程如下:
首先数据包到达网卡进行接收
调用系统的接收函数tcp_v4_rcv,tcp_v4_do_rcv,tcp_rcv_established
然后调用tcp_queue_rcv函数将数据保存到socket的接受队列中,通过对应的hash算法进行映射到对应的具体socket上
现在接收队列已经有了数据,系统内核将会调用一个回调函数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();
}
完整流程如下图
二、同步阻塞为什么性能差
进程间的切换太过频繁,而进程的切换是十分消耗资源的。
参考资料
深入理解Linu网络-张彦飞