Socket层实现系列 — 睡眠驱动的同步等待
主要内容:Socket的同步等待机制,connect和accept等待的实现。
内核版本:3.15.2
我的博客:http://blog.csdn.net/zhangskd
概述
socket上定义了几个IO事件:状态改变事件、有数据可读事件、有发送缓存可写事件、有IO错误事件。
对于这些事件,socket中分别定义了相应的事件处理函数,也称回调函数。
Socket I/O事件的处理过程中,要使用到sock上的两个队列:等待队列和异步通知队列,这两个队列中
都保存着等待该Socket I/O事件的进程。
Q:为什么要使用两个队列,等待队列和异步通知队列有什么区别呢?
A:等待队列上的进程会睡眠,直到Socket I/O事件的发生,然后在事件处理函数中被唤醒。
异步通知队列上的进程则不需要睡眠,Socket I/O事件发时,事件处理函数会给它们发送到信号,
这些进程事先注册的信号处理函数就能够被执行。
等待队列
Socket层使用等待队列来进行阻塞等待,在等待期间,阻塞在此socket上的进程会睡眠。
struct sock {
...
struct socket_wq __rcu *sk_wq; /* socket的等待队列和异步通知队列 */
...
} struct socket_wq {
/* Note: wait MUST be first field of socket_wq */
wait_queue_head_t wait; /* 等待队列头 */
struct fasync_struct *fasync_list; /* 异步通知队列 */
struct rcu_head *rcu;
};
(1) socket的等待队列头
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
(2) 进程的等待任务
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private; /* 指向当前的进程控制块 */
wait_queue_func_t func; /* 唤醒函数 */
struct list_head task_list; /* 用于链接入等待队列 */
};
typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t) (wait_queue_t *wait, unsigned mode, int flags, void *key);
int default_wake_function(wait_queue_t *wait, unsigned mode, int flags, void *key);
(3) 初始化等待任务
#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), \
} int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int ret = default_wake_function(wait, mode, sync, key); /* 默认的唤醒函数 */ if (ret)
list_del_init(&wait->task_list); /* 从等待队列中删除 */ return ret;
} int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags, void *key)
{
return try_to_wake_up(curr->private, mode, wake_flags);
}
try_to_wake_up()通过把进程的状态设置为TASK_RUNNING,并把进程插入CPU运行队列,来唤醒睡眠的进程。
(4) 把等待任务插入到等待队列中
获取sock的等待队列。
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;
}
把等待任务加入到等待队列中,同时设置当前进程的状态,TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。
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);
}
prepare_to_wait()和prepare_to_wait_exclusive()都是用来把等待任务加入到等待队列中,不同之处在于
使用prepare_to_wait_exclusive()时,会在等待任务中添加WQ_FLAG_EXCLUSIVE标志,表示一次只能
唤醒一个等待任务,目的是为了避免惊群现象。
void prepare_to_wait_exclusive(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_tail(q, wait); /* 把此等待任务加入到等待队列尾部 */ set_current_state(state); /* 设置当前进程的状态 */ spin_unlock_irqrestore(&q->lock, flags);
} static inline void __add_wait_queue_tail(wait_queue_head_t *head, wait_queue_t *new)
{
list_add_tail(&new->task_list, &head->task_list);
} #define set_current_state(state_value) \
set_mb(current->state, (state_value))
(5) 删除等待任务
从等待队列中删除等待任务,同时把等待进程的状态置为可运行状态,即TASK_RUNNING。
/**
* finish_wait - clean up after waiting in a queue
* @q: waitqueue waited on,等待队列头
* @wait: wait descriptor,等待任务
*
* Sets current thread back to running state and removes the wait
* descriptor from the given waitqueue if still queued.
*/
void finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
__set_current_state(TASK_RUNNING); if (! list_empty_careful(&wait->task_list)) {
spin_lock_irqsave(&q->lock, flags); list_del_init(&wait->task_list); /* 从等待队列中删除 */ spin_unlock_irqrestore(&q->lock, flags);
}
}
connect等待
(1) 睡眠
connect()的超时时间为sk->sk_sndtimeo,在sock_init_data()中初始化为MAX_SCHEDULE_TIMEOUT,
表示无限等待,可以通过SO_SNDTIMEO选项来修改。
static long inet_wait_for_connect(struct sock *sk, long timeo, int writebias)
{
DEFINE_WAIT(wait); /* 初始化等待任务 */ /* 把等待任务加入到socket的等待队列头部,把进程的状态设为TASK_INTERRUPTIBLE */
prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
sk->sk_write_pending += writebias; /* Basic assumption: if someone sets sk->sk_err, he _must_ change state of the socket
* from TCP_SYN_*. Connect() does not allow to get error notifications without closing
* the socket.
*/ /* 完成三次握手后,状态就会变为TCP_ESTABLISHED,从而退出循环 */
while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
release_sock(sk); /* 等下要睡觉了,先释放锁 */ /* 进入睡眠,直到超时或收到信号,或者被I/O事件处理函数唤醒。
* 1. 如果是收到信号退出的,timeo为剩余的jiffies。
* 2. 如果使用了SO_SNDTIMEO选项,超时退出后,timeo为0。
* 3. 如果没有使用SO_SNDTIMEO选项,timeo为无穷大,即MAX_SCHEDULE_TIMEOUT,
* 那么返回值也是这个,而超时时间不定。为了无限阻塞,需要上面的while循环。
*/
timeo = schedule_timeout(timeo); lock_sock(sk); /* 被唤醒后重新上锁 */ /* 如果进程有待处理的信号,或者睡眠超时了,退出循环,之后会返回错误码 */
if (signal_pending(current) || !timeo)
break; /* 继续睡眠吧 */
prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
} /* 等待结束时,把等待进程从等待队列中删除,把当前进程的状态设为TASK_RUNNING */
finish_wait(sk_sleep(sk), &wait);
sk->sk_write_pending -= writebias;
return timeo;
}
(2) 唤醒
三次握手中,当客户端收到SYNACK、发出ACK后,连接就成功建立了。
此时连接的状态从TCP_SYN_SENT或TCP_SYN_RECV变为TCP_ESTABLISHED,sock的状态发生变化,
会调用sock_def_wakeup()来处理连接状态变化事件,唤醒进程,connect()就能成功返回了。
sock_def_wakeup()的函数调用路径如下:
tcp_v4_rcv
tcp_v4_do_rcv
tcp_rcv_state_process
tcp_rcv_synsent_state_process
tcp_finish_connect
sock_def_wakeup
wake_up_interruptible_all
__wake_up
__wake_up_common
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
...
tcp_set_state(sk, TCP_ESTABLISHED); /* 在这里设置为连接已建立的状态 */
...
if (! sock_flag(sk, SOCK_DEAD)) {
sk->sk_state_change(sk); /* 指向sock_def_wakeup,会唤醒调用connect()的进程,完成连接的建立 */
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT); /* 如果使用了异步通知,则发送SIGIO通知进程可写 */
}
}
accept等待
(1) 睡眠
accept()超时时间为sk->sk_rcvtimeo,在sock_init_data()中初始化为MAX_SCHEDULE_TIMEOUT,表示无限等待。
/* Wait for an incoming connection, avoid race conditions.
* This must be called with the socket locked.
*/
static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
{
struct inet_connection_sock *icsk = inet_csk(sk);
DEFINE_WAIT(wait); /* 初始化等待任务 */
int err; for (; ;) {
/* 把等待任务加入到socket的等待队列中,把进程状态设置为TASK_INTERRUPTIBLE */
prepare_to_wait_exclusive(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); release_sock(sk); /* 等下可能要睡觉了,先释放 */ if (reqsk_queue_empty(&icsk->icsk_accept_queue)) /* 如果全连接队列为空 */
timeo = schedule_timeout(timeo); /* 进入睡眠直到超时或收到信号,或被IO事件处理函数唤醒 */ lock_sock(sk); /* 醒来后重新上锁 */
err = 0;
/* 全连接队列不为空时,说明有新的连接建立了,成功返回 */
if (! reqsk_queue_empty(&icsk->icsk_accept_queue))
break; err = -EINVAL;
if (sk->sk_state != TCP_LISTEN) /* 如果sock不处于监听状态了,退出,返回错误码 */
break; err = sock_intr_errno(timeo); /* 如果进程有待处理的信号,退出,返回错误码。
* 因为timeo默认为MAX_SCHEDULE_TIMEOUT,所以err默认为-ERESTARTSYS。
* 接下来会重新调用此函数,所以accept()依然阻塞。
*/
if (signal_pending(current))
break; err = -EAGAIN;
if (! timeo) /* 如果等待超时,即超过用户设置的sk->sk_rcvtimeo,退出 */
break;
} /* 从等待队列中删除等待任务,把等待进程的状态设为TASK_RUNNING */
finish_wait(sk_sleep(sk), &wait);
return err;
}
(2) 唤醒
三次握手中,当服务器端接收到ACK完成连接建立的时候,会把新的连接链入全连接队列中,
然后唤醒监听socket上的等待进程,accept()就能成功返回了。
三次握手时,当收到客户端的ACK后,经过如下调用:
tcp_v4_rcv
tcp_v4_do_rcv
tcp_child_process
sock_def_readable
wake_up_interruptible_sync_poll
__wake_up_sync_key
__wake_up_common
最终调用我们给等待任务注册的唤醒函数。
我们来看下accept()是如何避免惊群现象的。
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive,
int wake_flags, void *key)
{
wait_queue_t *curr, *next; list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags; if (curr->func(curr, mode, wake_flags, key) && (flags & WQ_FLAG_EXCLUSIVE)
!--nr_exclusive)
break;
}
}
初始化等待任务时,flags |= WQ_FLAG_EXCLUSIVE。传入的nr_exclusive为1,表示只允许唤醒一个等待任务。
所以这里只会唤醒一个等待的进程,不会导致惊群现象。
Socket层实现系列 — 睡眠驱动的同步等待的更多相关文章
- Socket层实现系列 — 信号驱动的异步等待
主要内容:Socket的异步通知机制. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 概述 socket上定义了几个IO事件:状态改变事件.有数据可读事 ...
- Socket层实现系列 — send()类发送函数的实现
主要内容:socket发送函数的系统调用.Socket层实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 发送流程图 以下是send().sendt ...
- Socket层实现系列 — connect()的实现
主要内容:connect()的Socket层实现.期间进程的睡眠和唤醒. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 应用层 int connect( ...
- Socket层实现系列 — accept()的实现(一)
本文主要介绍了accept()的系统调用.Socket层实现,以及TCP层实现. 内核版本:3.6 Author:zhangskd @ csdn blog 应用层 int accept(int soc ...
- Socket层实现系列 — I/O事件及其处理函数
主要内容:Socket I/O事件的定义.I/O处理函数的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd I/O事件定义 sock中定义了几个I/ ...
- Socket层实现系列 — getsockname()和getpeername()的实现
本文主要介绍了getsockname()和getpeername()的内核实现. 内核版本:3.6 Author:zhangskd @ csdn blog 应用层 int getsockname(in ...
- Socket层实现系列 — listen()的实现
本文主要分析listen()的内核实现,包括它的系统调用.Socket层实现.半连接队列,以及监听哈希表. 内核版本:3.6 Author:zhangskd @ csdn blog 应用层 int l ...
- Socket层实现系列 — bind()的实现(一)
bind()函数的使用方法很简单,但是它是怎么实现的呢? 笔者从应用层出发,沿着网络协议栈,分析了bind()的系统调用.Socket层实现,以及它的TCP层实现. 本文主要内容:bind()的系统调 ...
- Socket层实现系列 — accept()的实现(二)
本文主要分析accept()的阻塞等待和唤醒. 内核版本:3.6 Author:zhangskd @ csdn blog 等待队列 (1)socket的等待队列 /* * @sk_wq: sock w ...
随机推荐
- JVM内存模型及分区
Java虚拟机在程序执行过程会把jvm的内存分为若干个不同的数据区域来管理,这些区域有自己的用途,以及创建和销毁时间. JVM内存模型如下图所示: jvm管理的内存区域包括以下几个区域: 栈区: 栈 ...
- 利用gulp把本地文件移动到指定待发布文件夹
一.目标 把本地的文件移动到待发布的文件中,把static_grab文件中file.txt所列文件列表移动到beta对应文件夹中: 二.实现 var gulp = require('gulp'), w ...
- Tomcat常用参数的配置
1.修改端口号 Tomcat端口配置在server.xml文件的Connector标签中,默认为8080,可根据实际情况修改. 修改端口号 2.解决URL中文参数乱码 在server.xml文件的Co ...
- 81. Search in Rotated Sorted Array II (中等)
Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand. (i.e. ...
- 学习在.NET Core中使用RabbitMQ进行消息传递之持久化(二)
前言 上一节我们简单介绍了RabbitMQ和在安装后启动所出现的问题,本节我们开始正式进入RabbitMQ的学习,对于基本概念请从官网或者其他前辈博客上查阅,我这里不介绍基础性东西,只会简单提一下,请 ...
- ACM Bone Collector
Many years ago , in Teddy's hometown there was a man who was called "Bone Collector". Th ...
- 终止Docker容器
可以使用 docker stop 来终止一个运行中的容器. 此外,当Docker容器中指定的应用终结时,容器也自动终止. 例如对于上一章节中只启动了一个终端的容器,用户通过 exit 命令或 Ctrl ...
- XCode使用技巧
XCode使用技巧 自动生成get.set方法 @property 用法 #import <Foundation/Foundation.h> @interface People : NSO ...
- 微信小程序基础之input输入框控件
今天主要详写一下微信小程序中的Input输入框控件,输入框在程序中是最常见的,登录,注册,获取搜索框中的内容等等都需要,同时,还需要设置不同样式的输入框,今天的代码中都要相应的使用. input输入框 ...
- 多线程(三) 实现线程范围内模块之间共享数据及线程间数据独立(ThreadLocal)
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.JDK 1.2的版本中就提供java.lang.ThreadLocal,使用这个工具类可以很简洁地编写出优美的多线程程序,Threa ...