深入理解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,进程将进⼊睡眠状态,这会导致⼀次进程上下⽂的开销。
原文地址:https://blog.csdn.net/m0_55334946/article/details/140563035
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!