很久以前,我在开发一个TCP拥塞控制算法模块的时候,由于频繁快速迭代,常常需要将模块卸了再装,装了再卸,由于我更改的Linux的全局内核参数net.ipv4.tcp_congestion_control,我的ssh连接也是要使用同样的CC模块,这导致在我将默认算法切换到reno builtin算法后,必须将所有TCP连接全部断开后才能使得CC模块的引用计数降为0,从而顺利卸载。

以bic为例,我将tcp_bic模块加载后,连入几个TCP连接:

[root@localhost ~]# sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = bic
[root@localhost ~]# lsmod |grep bic
tcp_bic                13483  3

你看,tcp_bic的引用计数为3,这意味着有3个连接在使用该算法:

[root@localhost ~]# ss -antip|awk -F ' ' '/ bic/{print a;}{a=$6}'
users:(("sshd",pid=10323,fd=3))
users:(("sshd",pid=10515,fd=3))
users:(("sshd",pid=10348,fd=3))

如何在不杀掉这三个进程的前提下,卸载掉tcp_bic模块呢?

本文就是说这个的,哈哈。

思路很简单,将这三个进程的CC算法切换成reno这个builtin算法不就可以了吗?

谈何容易?虽然socket支持TCP_CONGESTION这个sockopt,但它需要在进程上下文设置,这需要动态hook进程,这三个进程大部分时间都在wait,我们可以随便找一个看一下:

[root@localhost ~]# cat /proc/10348/wchan
poll_schedule_timeout
[root@localhost ~]#

这意味着我们需要hook住其select/poll/epoll等调用,然后插入setsockopt调用。

复杂,且无趣。

下面我来杂耍一种基于systemtap的方法。

完成两个目标即可:

  • 想办法切到目标进程的上下文。
  • 在目标进程上下文完成setsockopt。

第二个目标手到擒来,问题是第一个目标如何实现。

也不难。

我们知道,所有进程被切换出去都是从__schedule进入,而它再次被切换回来则从__schedule出来,因此只需要hook __schedule.return即可,然后wakeup目标进程。

这会导致wait在select/poll/epoll的目标进程被唤醒,然后检查资源未就绪后再次被切换出去,我们的机会正在其中间,即__schedule返回的一刹那。

代码就是下面的样子:

#!/usr/bin/stap -g

%{
#include <net/sock.h>
%}

function alter_cc(fd:long)
%{
	int err;
	mm_segment_t fs;
	char cc[] = "reno";
	struct socket *socket = NULL;

	socket = sockfd_lookup(STAP_ARG_fd, &err);
	if (socket == NULL) {
		return;
	}

	fs = get_fs();
	set_fs(KERNEL_DS);
#define TCP_CONGESTION	13
	sock_common_setsockopt(socket, SOL_TCP, TCP_CONGESTION, (void*)cc, strlen(cc));
	set_fs(fs);

	sockfd_put(socket);
%}

probe kernel.function("__schedule").return
{
	if (pid() == $1) {
		alter_cc($2);
		exit()
	}
}

function wakeup(pid:long)
%{
	struct task_struct *tsk;

	tsk = pid_task(find_vpid(STAP_ARG_pid), PIDTYPE_PID);
	if (tsk)
		wake_up_process(tsk);
%}

probe timer.ms(500)
{
	wakeup($1)
}

对,就是这么简单。来来来,看效果:

[root@localhost ~]# lsmod |grep tcp_bic
tcp_bic                13483  3
[root@localhost ~]# ss -antip|awk -F ' ' '/ bic/{print a;}{a=$6}'
users:(("sshd",pid=11707,fd=3))
users:(("sshd",pid=11680,fd=3))
users:(("sshd",pid=11654,fd=3))
[root@localhost ~]# ss -antip|awk -F ' ' '/ bic/{print a;}{a=$6}'|egrep -o [0-9]+ |sed 'N;s/\n/ /g' |xargs -L 1 ./alterCC.stp
[root@localhost ~]# ss -antip|awk -F ' ' '/ bic/{print a;}{a=$6}'
[root@localhost ~]# lsmod |grep tcp_bic
tcp_bic                13483  0
[root@localhost ~]# rmmod tcp_bic
[root@localhost ~]# echo $?
0
[root@localhost ~]# lsmod |grep tcp_bic

一气呵成,不多说。

可以确认下当前这些进程的CC算法是不是已经改成了reno,于是我用reno来正则匹配:

[root@localhost ~]# ss -antip|awk -F ' ' '/reno/{print a;}{a=$6}'
users:(("sshd",pid=11707,fd=3))
users:(("sshd",pid=11680,fd=3))
users:(("sshd",pid=11654,fd=3))

好吧,我承认这里的ss/egrep/sed/xargs写的有点low,如果有谁能帮我写个优雅的,我感激不尽。

那么,再杂耍一个crash extension插件来遍历所有TCP连接的CC算法,可以的,代码如下:


static int get_field(unsigned long addr, char *name, char *field, void* buf)
{
	unsigned long off = MEMBER_OFFSET(name, field);

	if (!readmem(addr + off, KVADDR, buf, MEMBER_SIZE(name, field), name, FAULT_ON_ERROR))
		return 0;
	return 1;
}

struct dummy_list {
	struct dummy_list *stub;
};

struct dummy_list *iter;
void do_cmd(void)
{
	unsigned long _addr, sock_addr, cc_addr, socket_addr, inode_addr, next;
	unsigned long ehash_mask = 0, inode;
	char name[64];
	int i;

	optind++;
	iter = (struct dummy_list *)htol(args[optind], FAULT_ON_ERROR, NULL);
	optind++;
	ehash_mask = atoi(args[optind]);

	for (i = 0; i <= ehash_mask; i++) {
		next = (unsigned long)iter + i*sizeof(unsigned long);
		do {
			inode = 0;
			get_field(next, "hlist_nulls_node", "next", &_addr);
			if (_addr & 0x1) {
				break;
			}
			sock_addr = _addr - MEMBER_OFFSET("sock_common", "skc_nulls_node");
			get_field(sock_addr, "inet_connection_sock", "icsk_ca_ops", &cc_addr);
			get_field(cc_addr, "tcp_congestion_ops", "name", &name[0]);
			get_field(sock_addr, "sock", "sk_socket", &socket_addr);
			if (socket_addr) {
				inode_addr = socket_addr + MEMBER_OFFSET("socket_alloc", "vfs_inode");
				get_field(inode_addr, "inode", "i_ino", &inode);
			}
			fprintf(fp, "  ----%s   %d\n", name, inode);
		} while(get_field(next, "hlist_nulls_node", "next", &next));
	}
}

static struct command_table_entry command_table[] = {
	{ "tcpcc", do_cmd, NULL, 0},
	{ NULL },
};

void __attribute__((constructor)) tcpcc_init(void)
{
	register_extension(command_table);
}

void __attribute__((destructor)) tcpcc_fini(void) { }

编译之:

[root@localhost ext]# gcc -fPIC -shared tcpcc.c -o tcpcc.so

在crash命令行载入,执行之:

crash>
crash> extend  tcpcc.so
./tcpcc.so: shared object loaded
crash> px tcp_hashinfo.ehash
$5 = (struct inet_ehash_bucket *) 0xffffc90000188000
crash> pd tcp_hashinfo.ehash_mask
$6 = 8191
crash>
crash> tcpcc 0xffffc90000188000 8191
  ----reno   27670
  ----reno   27547
  ----reno   27407
  ----reno   36423
  ----reno   28717
  ----reno   36282
  ----reno   36138
crash>

以上输出的第二列是socket的inode号,这个用来和procfs里task的fd来匹配,最终的交集配合pid来执行alterCC.stp脚本。

唉,又将是一堆可怕的命令组合。


浙江温州皮鞋湿,下雨进水不会胖。

Logo

更多推荐