kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

sk 的锁,spin_lock_bh、lock_sock

一、修改sk的锁 sk_lock.slock

tcp协议栈对struct sock sk有两把锁,第一把是sk_lock.slock,第二把则是sk_lock.owned。sk_lock.slock用于获取struct sock sk对象的成员的修改权限;sk_lock.owned用于区分当前是进程上下文或是软中断上下文,为进程上下文时sk_lock.owned会被置1,中断上下文为0。

如果是要对sk修改,首先是必须拿锁sk_lock.slock,其后是判断当前是软中断或是进程上下文,如果是进程上下文,那么一般也不能修改sk

中断上下文可以用下面的锁,也就是下面的锁只有 spin_lock

1
2
3
4
5
6
/* 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))

非中断上下文可以直接用 spin_lock_bh(&((sk)->sk_lock.slock))

获得sk_lock.slock 锁后还要判断 sock_owned_by_user(sk), 如果被进程上下文占用也一般不能操作sk

1
#define sock_owned_by_user(sk)  ((sk)->sk_lock.owned)

二、进程上下文获取sk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static inline void lock_sock(struct sock *sk)
{
	lock_sock_nested(sk, 0);
}

void lock_sock_nested(struct sock *sk, int subclass)
{
	might_sleep();

	// 先获取 sk_lock.slock 锁
	spin_lock_bh(&sk->sk_lock.slock);
	// 如果有进程占用sk,则调__lock_sock等待占用sk的进程结束
	if (sk->sk_lock.owned)
		__lock_sock(sk);

	// 此时占用sk的进程结束了,但前进程就可以占用sk
	sk->sk_lock.owned = 1;
	// 进程占用sk时不占 sk_lock.slock
	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();
}

static void __lock_sock(struct sock *sk)
	__releases(&sk->sk_lock.slock)
	__acquires(&sk->sk_lock.slock)
{
	DEFINE_WAIT(wait);

	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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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);
	if (sk->sk_backlog.tail)
		__release_sock(sk); // 处理backlog的包

	/* Warning : release_cb() might need to release sk ownership,
	 * ie call sock_release_ownership(sk) before us.
	 */
	if (sk->sk_prot->release_cb)
		sk->sk_prot->release_cb(sk);

	// 获取sk_lock.slock,然后清除 sk_lock.owned
	sock_release_ownership(sk);
	if (waitqueue_active(&sk->sk_lock.wq))
		wake_up(&sk->sk_lock.wq);
	spin_unlock_bh(&sk->sk_lock.slock);
}

static inline void sock_release_ownership(struct sock *sk)
{
	sk->sk_lock.owned = 0;
}

本地IP包分片--local_df,ignore_df

local_df 和 ignore_df 是一个意思,在某个版本rename了

ip_queue_xmit 函数中有:

1
2
3
4
if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
	iph->frag_off = htons(IP_DF);
else
	iph->frag_off = 0;

ip_dont_fragment

1
2
3
4
5
6
7
static inline
int ip_dont_fragment(struct sock *sk, struct dst_entry *dst)
{
	return  inet_sk(sk)->pmtudisc == IP_PMTUDISC_DO ||
		(inet_sk(sk)->pmtudisc == IP_PMTUDISC_WANT &&
		 !(dst_metric_locked(dst, RTAX_MTU)));
}

一般情况下都是开启pmtu、skb->ignore_df = 0, 所以 iph->frag_off = htons(IP_DF);

ip_queue_xmit -> ip_finish_output -> ip_fragment :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int ip_fragment(struct sock *sk, struct sk_buff *skb,
	       unsigned int mtu, 
	       int (*output)(struct sock *, struct sk_buff *))
{
	struct iphdr *iph = ip_hdr(skb);

	// 如果需要分片,直接进入分片函数
	if ((iph->frag_off & htons(IP_DF)) == 0)
		return ip_do_fragment(sk, skb, output);

	// 如果没设置分片,或手动设置的分片过大,则直接丢弃
	if (unlikely(!skb->ignore_df ||
		     (IPCB(skb)->frag_max_size &&
		      IPCB(skb)->frag_max_size > mtu))) {
		struct rtable *rt = skb_rtable(skb);
		struct net_device *dev = rt->dst.dev;

		IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
		icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
			  htonl(mtu));
		kfree_skb(skb);
		return -EMSGSIZE;
	}

	// 所以设置 skb->ignore_df = 1 且 skb->len > mtu 则执行到这里
	return ip_do_fragment(sk, skb, output);
}

所以设置 skb->ignore_df = 1 且 skb->len > mtu 则执行 ip_do_fragment 进行IP分片,

分片是按网卡mtu进行,如果mss小于网卡mtu-40,则需要设置 IPCB(skb)->frag_max_size

iph->frag_off 定义

1
2
3
#define IP_DF           0x4000          /* Flag: "Don't Fragment"       */
#define IP_MF           0x2000          /* Flag: "More Fragments"       */
#define IP_OFFSET       0x1FFF          /* "Fragment Offset" part       */

1) 不分片的包 iph->frag_off = htons(IP_DF)
2) 最后一个分片包 ((ntohs(iph->frag_off) & IP_OFFSET) > 0)
3) 其余分片包 ((ntohs(iph->frag_off) & IP_OFFSET) > 0 && (iph->frag_off & htons(IP_MF)) > 0)

拥塞控制模块无法卸载

如果listen的sk设置了icsk_ca_setsockopt,那么派生的child默认都是sk的cong,而不是系统默认的cong


如果:自定义拥塞控制中维护了hash表,且在init时加入hash表,在release时删除或卸载清理hash表。

那么:有些情况会导致分配了拥塞控制模块给sk,但却没调init,导致卸载清理时就找不到模块被哪个sk引用,也就无法清理。

可能原因:

1. 拥塞控制初始化时

标准内核:socket()时分配拥塞控制,但在连接建立时才会调 icsk->icsk_ca_ops->init(sk) 如果只调了 socket(), bind(), listen() 那么就不会调 icsk->icsk_ca_ops->init(sk)(所以listen的sk就一定不调init)。

内核可行修改方案:socket -> tcp_init_sock -> tcp_assign_congestion_control 时先分配一个内核内部的拥塞控制,连接建立时再分配实际拥塞控制,然后初始化。 这么改还是有缺陷:先调 socket(),再调 setsockopt 设置拥塞控制模块,因为此时sk->sk_state = TCP_CLOSE,所以不会调init

2. 拥塞控制切换时

拥塞控制切换时:如果是TCP_CLOSE状态也不会调 icsk->icsk_ca_ops->init(sk) 初始化

可行解决方法

模仿 /proc/net/tcp 遍历hash表,修改拥塞控制

特殊情况:

调 socket 时 sk 不加入hash表,在 listen 或 connect 时才会加入到hash表,如果只调socket、bind然后不再使用该fd,那么sk不仅没调icsk->icsk_ca_ops->init(sk),也没加入hash表。

此时 /proc/net/tcp 也找不到sk,只能遍历所有进程的所有fd,找出sk再修改

conglist.tar.gz