深入理解Linux网络(四):TCP接收阻塞

TCP socket 接收函数 recv 发出 recvfrom 系统调用。
进⼊系统调⽤后,⽤户进程就进⼊到了内核态,通过执⾏⼀系列的内核协议层函数,然后到 socket 对象的接收队列中查看是否有数据,没有的话就把⾃⼰添加到 socket 对应的等待队列⾥。最后让出CPU,操作系统会选择下⼀个就绪状态的进程来执⾏。
在这里插入图片描述
假如我们没有使⽤ O_NONBLOCK 标记,等待接收的过程会阻塞进程,但是我们先探究阻塞的过程。

//file: net/socket.c
SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t,
 size, unsigned int, flags, struct sockaddr __user *, addr,
 int __user *, addr_len)
{
 struct socket *sock;
 //根据⽤户传⼊的 fd 找到 socket 对象
 sock = sockfd_lookup_light(fd, &err, &fput_needed);
 ......
 err = sock_recvmsg(sock, &msg, size, flags);
 ......
}

sock_recvmsg -> __sock_recvmsg -> __sock_recvmsg_nosec

static inline int __sock_recvmsg_nosec(struct kiocb *iocb, struct socket *sock,
 struct msghdr *msg, size_t size, int flags)
{
 ......
 return sock->ops->recvmsg(iocb, sock, msg, size, flags);
}

在之前的 socket 对象图中,从图中看到 recvmsg 指向的是 inet_recvmsg 方法。
在这里插入图片描述

//file: net/ipv4/af_inet.c
int inet_recvmsg(struct kiocb *iocb, struct socket *sock,
struct msghdr *msg, size_t size, int flags)
{
 ...
 err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT,
 flags & ~MSG_DONTWAIT, &addr_len);

这里又出现了一个 recvmsg 函数指针,不过这个是socket 对象中的 recvmsg 方法,对应 TCP 协议的 tcp_recvmsg 方法。

//file: net/ipv4/tcp.c
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
 size_t len, int nonblock, int flags, int *addr_len)
{
 int copied = 0;
 ...
 do {
  //遍历接收队列接收数据
  skb_queue_walk(&sk->sk_receive_queue, skb) {
  ...
 }
 ...
 }
 if (copied >= target) {
  release_sock(sk);
  lock_sock(sk);
  } else //没有收到⾜够数据,启⽤ sk_wait_data 阻塞当前进程
  sk_wait_data(sk, &timeo);
}

可以看到,消息量不够,一样也会阻塞。
在这里插入图片描述
如果没有收到数据,或者收到不⾜够多,则调⽤ sk_wait_data 把当前进程阻塞掉。

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

sk_wait_data 阻塞进程的实现:
在这里插入图片描述
做完排队工作后,给所在进程改个状态位即可。
⾸先在 DEFINE_WAIT 宏下,定义了⼀个等待队列项 wait。 在这个新的等待队列项上,注册了回调函数 autoremove_wake_function,并把当前进程描述符 current 关联到其 .private 成员上。

//file: include/linux/wait.h
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
#define DEFINE_WAIT_FUNC(name, function) \
 wait_queue_t name = { \
 .private = current, \
 .func = function, \
 .task_list = LIST_HEAD_INIT((name).task_list), \
 }

紧接着在 sk_wait_data 中 调⽤ sk_sleep 获取 sock 对象下的等待队列列表头 wait_queue_head_t。
sk_sleep 源代码如下:

//file: include/net/sock.h
static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
 BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
 return &rcu_dereference_raw(sk->sk_wq)->wait;
}

接着调⽤ prepare_to_wait 来把新定义的等待队列项 wait 插⼊到 sock 对象的等待队下。

//file: kernel/wait.c
void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
 unsigned long flags;
 wait->flags &= ~WQ_FLAG_EXCLUSIVE;
 spin_lock_irqsave(&q->lock, flags);
 if (list_empty(&wait->task_list))
 __add_wait_queue(q, wait);
 set_current_state(state);
 spin_unlock_irqrestore(&q->lock, flags);
}

这样后⾯当内核收完数据产⽣就绪时间的时候,就可以查找 socket 等待队列上的等待项,进⽽就可以找到回调函数和在等待该 socket 就绪事件的进程了。
最后再调⽤ sk_wait_event 让出 CPU,进程将进⼊睡眠状态,这会导致⼀次进程上下⽂的开销。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值