卸载正在使用中的tcp_congestion_control模块
很久以前,我在开发一个TCP拥塞控制算法模块的时候,由于频繁快速迭代,常常需要将模块卸了再装,装了再卸,由于我更改的Linux的全局内核参数net.ipv4.tcp_congestion_control,我的ssh连接也是要使用同样的CC模块,这导致在我将默认算法切换到reno builtin算法后,必须将所有TCP连接全部断开后才能使得CC模块的引用计数降为0,从而顺利卸载。以bic为例,我将t
很久以前,我在开发一个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脚本。
唉,又将是一堆可怕的命令组合。
浙江温州皮鞋湿,下雨进水不会胖。
更多推荐
所有评论(0)