Switch-Router

socket的加锁与解锁

Published at 2021-01-30 | Last Update 2021-01-30

在内核网络系统代码中,我们可以见到很多地方都有 lock_sock() 这类对 socket 结构的加锁操作,而也有少数地方是使用 bh_lock_sock()

那么什么时候该用 lock_sock(),什么时候又该用 bh_lock_sock() ?

本文结合实际内核代码稍加解释。

(本文使用内核代码版本是 4.19.75,不过这个部分变化不大,其他版本内核也ok)

0x0. process context 与 interrupt context

我们通常将内核代码执行的上下文分为 process context 和 interrupt context 。

当用户态进程通过系统调用(比如 sys_socket、sys_bind 等) 进入内核空间后,我们认为此时内核依然处于 process context。处于 process context 的一个特征是 current 这个指向当前 task_struct 的变量是有意义的,也就是说此刻我们明确指出这一段代码是正在被哪一个进程执行。

与之相对的则是 interrupt context, interrupt context 没有属于哪一个进程的概念,此时 current 变量是无效的。

什么情况属于 interrupt context 呢?中断响应处理函数(interrupt handler)是一种,除此之外,softirq 和 tasklet 这类下半部处理(bottom half)的运行上下文也属于 interrupt context。

0x1. context 之间的互斥

我们通常会使用’锁’去保护那些可能被多个 task_struct 访问的资源。在本文的范围内,我们将锁分为始终占用CPU忙等的 spinlock 和可以睡眠让出CPU的 semaphore 两种。

spinlock 在 process context 和 interrupt context 都可以使用(只要你愿意),而 semaphore 则不能在 interrupt context 使用,本质原因是处于 interrupt context 时,current 是无效的,即它不和任何 task_struct 联系在一起!回想一下我们的睡眠,实际上是指将当前task_struct放到一个队列上,然后让出CPU,等时间到时从这个队列上 wake up这个task_struct。而 interrupt context 压根没有关联的 task_struct,也就不存在睡眠了。

具体到本文而言,锁的保护场景分为了以下 3 种

  • (1) process context 与 process context 的互斥
  • (2) process context 与 interrupt context 的互斥
  • (3) interrupt context 与 interrupt context 的互斥

(1) 和 (3) 的情况比较简单。既然 process context中可以睡眠,那么我们可以放心地用 semaphore,当然用 spinlock 也未尝不可。而(3)则只能用 spinlock,也没得选。

而情形(2)仔细想想,实际上也只能用 spinlock,原因还是 interrupt context 只能使用 spinlock。

0x2. spin_lock_xxx 的多种变体

spinlock 在内核中有多种变体:

  • spin_lock/spin_unlock
  • spin_lock_bh/spin_unlock_bh
  • spin_lock_irq/spin_unlock_irq
  • spin_lock_irqsave/spin_unlock_irqrestore

结论一:如果一个 spinlock 会在 interrupt handler 中使用,则其他地方对这个 spinlock 加锁之前都需要关闭本地中断。

如果不关闭本地中断,当获得了该 spinlock 之后,本地中断到了,interrupt handler 中也去尝试获取这个 spinlock 注定永远获取不到。这里仅仅限制是关闭”本地”中断,因为如果是其他CPU执行interrupt handler,还是可以等待并最终获取到 spinlock 的.

结论二:spin_lock_irqsave 和 spin_lock_irq 其实是 spin_lock 加上关闭中断的打包操作

这两个变体的目的就是为了防止忘记关闭本地中断. 前者还可以保存原有的中断使能状态,最后恢复原状,这是内核推荐的做法。

unsigned long flags;
spin_lock_irqsave(&mr_lock, flags); 
/* critical region ... */
spin_unlock_irqrestore(&mr_lock, flags);

结论三:spin_lock_bh 是在获取 spin_lock 的基础上禁用本地 bottom half。

原因和结论一类似,因为 process context 有可能被 bottom half context 抢占。如果一个资源在 bottom half context 和 process context 之间共享,假设 process context 先获取到 spinlock ,此时抢占发生,bottom half context 就会永远获取不到 spinlock。

简而言之,context 的优先级从 low 到 high 的顺序是: process context < bottom half context < interrupt handler context。

如果 low-priority context 会与 high-priority context 共享资源,则当 low-priority context 在获取 spinlock 前,都需要禁止本地 CPU上 的 high-priority context

0x3. socket 锁的设计

socket锁是为了保护内核 struct sock 结构中的一些数据在 process context 和 bottom half context 同时访问而设置的

定义为

typedef struct {
	spinlock_t  slock; 
	int			owned;  
	wait_queue_head_t	wq;
} socket_lock_t;

struct sock {
    // 
	socket_lock_t  sk_lock;
    // .....
}

为什么要有这么奇怪的定义呢?答案是为了性能

struct sock 结构可能在 process context 和 bottom half context 中被同时访问。比如报文的接收过程中,协议栈部分的代码是在 bottom half context下执行,而与此同时,用户完全可能在执行 setsocketopt 这类操作,后者是属于 process context。

虽然 bottom half 已经不是最要紧的工作(相对于 interrupt handler),但毕竟属于interrupt context,我们还是希望不要被阻塞太久,因此,内核对 socket 锁的设计要求准则:

  • 不要阻塞 bottom half context
  • 可以阻塞 process context

于是,便有了 lock_sock() 和 bh_lock_sock(),它们被分别用在 process context 和 bottom half context

bh_lock_sock()的实现很简单,就是对 spinlock 的加锁。

/* include/net/sock.h */
/* 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))

而 lock_sock() 则不同,它需要判断 process context 是否原本就占有(own)这个 sock 结构。如果是,则表示这个 sock 正在被被其他process占有,显然这时需要睡眠等待,否则设置 owned = 1,来宣告自己占有了这个 sock 结构。owned 这个字段的意思是 “owned by process context”

/* net/core/sock.c */
// process context use this to lock sock
void lock_sock_nested(struct sock *sk, int subclass)
{
	might_sleep();
	spin_lock_bh(&sk->sk_lock.slock);   // spin lock
	if (sk->sk_lock.owned)              // has already owned by user
		__lock_sock(sk);            // wait
	sk->sk_lock.owned = 1;
	spin_unlock(&sk->sk_lock.slock);

    mutex_acquire(&sk->sk_lock.dep_map, subclass, 0, _RET_IP_);
    local_bh_enable();
 }

是否被 process context 占有会影响 bottom half context 的执行过程,在 tcp 接收过程中(bottom half context),如果发现 sock 没有被process context占有,则继续进行TCP层的接收过程;如果 已经被占有,则会将报文临时保存在 sock 的 backlog 队列中,不会阻塞在这里

/* net/ipv4/tcp_ipv4.c */
bh_lock_sock_nested(sk);   
......
if (!sock_owned_by_user(sk)) {
	ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
	goto discard_and_relse;
}
bh_unlock_sock(sk);

之后,等到process context解除sock占有,报文的接收过程才会继续。

/* net/core/sock.c */
void release_sock(struct sock *sk)
{
	spin_lock_bh(&sk->sk_lock.slock);
	if (sk->sk_backlog.tail)    
		__release_sock(sk);  //  continue recv procedure

	// .....
}