linux内核中的struct sock结构是L3、L4和套接口层中的一个重要结构,代表了一个套接字,该结构在进程上下文和软中断上下文都会被使用,所以保证这些场景下该结构数据的一致性将非常重要,而且理解其锁机制对于理解代码也非常有意义,这篇笔记就记录了内核中该结构的同步锁。
1. 锁结构
struct sock {
...
socket_lock_t sk_lock;
...
}
传输控制块中的sk_lock成员用来保护对结构的访问。
/* This is the per-socket lock. The spinlock provides a synchronization
* between user contexts and software interrupt processing, whereas the
* mini-semaphore synchronizes multiple users amongst themselves.
*/
typedef struct {
spinlock_t slock;
int owned;
wait_queue_head_t wq;
/*
* We express the mutex-alike socket_lock semantics
* to the lock validator by explicitly managing
* the slock as a lock variant (in addition to
* the slock itself):
*/
//用于调试,忽略。该宏定义的有bug,在代码实现时对该字段的访问并没有用宏控制
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} socket_lock_t;
- slock:该自旋锁是用于同步进程上下文和软中断上下文的关键;
- owned:取值为1表示该传输控制块已经被进程上下文锁定,取值为0表示没有被进程上下文锁定;
- wq:等待队列,当进程上下文需要持有该传输控制块,但是其当前又被软中断锁定时,进程会等待
下面看看这三个字段到底是如何实现进程上下文和软中断上下文的同步访问的。
2. 进程上下文的访问操作
进程上下文在访问该传输控制块之前需要调用lock_sock()锁定,在访问完成后调用release_sock()将其释放,这两个函数的实现如下:
2.1 lock_sock()
static inline void lock_sock(struct sock *sk)
{
lock_sock_nested(sk, 0);
}
void lock_sock_nested(struct sock *sk, int subclass)
{
//注意:调用lock_sock()可能会导致休眠
might_sleep();
//持有自旋锁并关闭下半部
spin_lock_bh(&sk->sk_lock.slock);
//如果owned不为0,说明有进程持有该传输控制块,调用__lock_sock()等待,见下文
if (sk->sk_lock.owned)
__lock_sock(sk);
//上面__lock_sock()返回后现场已经被还原,即持有锁并且已经关闭下半部。
//将owned设置为1,表示本进程现在持有该传输控制块
sk->sk_lock.owned = 1;
//释放锁但是没有开启下半部
spin_unlock(&sk->sk_lock.slock);
/*
* The sk_lock has mutex_lock() semantics here:
*/
mutex_acquire(&sk->sk_lock.dep_map, subclass, 0, _RET_IP_);
//开启下半部
local_bh_enable();
}
//__lock_sock()将进程挂到sk->sk_lock中的等待队列wq上,直到没有进程再持有该该传输
//控制块时返回。注意:调用时已经持有sk->sk_lock,睡眠之前释放锁,返回前再次持有锁
static void __lock_sock(struct sock *sk)
{
//定义一个等待队列结点
DEFINE_WAIT(wait);
//循环,直到sock_owned_by_user()返回0才结束
for (;;) {
//将调用进程挂接到锁的等待队列中
prepare_to_wait_exclusive(&sk->sk_lock.wq, &wait,
TASK_UNINTERRUPTIBLE);
//释放锁并打开下半部
spin_unlock_bh(&sk->sk_lock.slock);
//执行一次调度
schedule();
//再次被调度到时会回到这里,首先持锁并关闭下半部
spin_lock_bh(&sk->sk_lock.slock);
//如果没有进程再次持有该传输控制块,那么返回
if (!sock_owned_by_user(sk))
break;
}
finish_wait(&sk->sk_lock.wq, &wait);
}
#define sock_owned_by_user(sk) ((sk)->sk_lock.owned)
从上面的代码实现中可以看出:
- 在获取锁时,逻辑是检查owned字段是否为1,如果该字段不为0,那么说明已经有进程持有该锁,这时调用进程需要会休眠并等待该字段变为0;如果owned值为0,那么说明没有进程持有该锁,直接设置owned为1。可见获取锁的终极目的就是要将owned字段设置为1,但是对owned字段的访问(查询和修改)是受自旋锁sk->sk_lock保护的。
- 一个很重要的事实是修改owned为1之后不再持有自旋锁,也已经开启了下半部。这种设计的好处是协议栈的处理并非立刻就能结束,如果只是简单的在开始起持有自旋锁并关闭下半部,在处理结束时释放自旋锁并打开下半部,会降低系统性能,更要命的是长时间关闭下半部,还可能使得网卡接收软中断得不到及时调用,导致丢包。
release_sock()
进程上下文在结束传输控制块的操作之后,需要调用release_sock()释放传输控制块。可以想象的到,释放的核心是将owned设置为0并通知其它等待该传输控制块的进程。下面看代码实现。
void release_sock(struct sock *sk)
{
/*
* The sk_lock has mutex_unlock() semantics:
*/
//调试相关,忽略
mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_);
//获取自旋锁并关闭下半部
spin_lock_bh(&sk->sk_lock.slock);
//如果后备队列不为空,则调用__release_sock()处理后备队列中的数据包,见数据包的接收过程
if (sk->sk_backlog.tail)
__release_sock(sk);
//设置owned为0,表示调用者不再持有该传输控制块
sk->sk_lock.owned = 0;
//如果等待队列不为空,则唤醒这些等待的进程
if (waitqueue_active(&sk->sk_lock.wq))
wake_up(&sk->sk_lock.wq);
//释放自旋锁并开启下半部
spin_unlock_bh(&sk->sk_lock.slock);
}
3. 软中断上下文的访问操作
在TCP的接收处理过程中有如下代码片段:
int tcp_v4_rcv(struct sk_buff *skb)
{
...
process:
...
//获取sk->sk_lock.slock自旋锁
bh_lock_sock_nested(sk);
//如果没有进程锁定该传输控制块,将数据接收到奥prequeue或者receive_queue中
if (!sock_owned_by_user(sk)) {
if (!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb);
} else
//如果进程已经锁定该传输控制块,那么先将数据接收到后备队列中
sk_add_backlog(sk, skb);
//释放自旋锁
bh_unlock_sock(sk);
...
/* BH context may only use the following locking interface. */
#define bh_lock_sock(__sk) spin_lock(&((__sk)->sk_lock.slock))
#define bh_lock_sock_nested(__sk) \
spin_lock_nested(&((__sk)->sk_lock.slock), \
SINGLE_DEPTH_NESTING)
#define bh_unlock_sock(__sk) spin_unlock(&((__sk)->sk_lock.slock))
从上面的代码中可以看出,在软中断上下文操作传输控制块时,是持有自旋锁的,因为软中断的处理代码会尽可能的快的退出,所以这是ok的。
遗留问题:nested版本的spin_lock()到底代表什么意思,这点还没有弄明白。
5. 传输控制块引用计数
这里还需要提的一点是传输控制块的销毁与否和上面说的同步锁没有直接的联系,是否销毁是由其引用计数成员sk_refcnt决定的,这是一个原子变量,可使用sock_get()和sock_put()增加和减少其引用计数。
6. 总结
对传输控制块成员进行访问时,如果只是访问那些一旦创建就不会再变的成员,那么只要保证在访问期间该传输控制块不被释放就行(引用计数不会被减为0),没有必要一定要持有该传输控制块。但是如果要访问那些可变的成员就必须要先锁定。牢记这个原则非常重要。