在运行一些非root用户进程的时候,我们都习惯要在前面加上一个ulimit -HSn 65535的命令。而且我们还知道关于文件描述符的限制,不止这一个地方,还有limits.conf,sysctl -w fs.file-max等等。但是到底这些是什么个关系呢?而且,如果是一个已经在运行的程序,有没有可能在更改他的文件描述符限制呢?
以最经常碰到这种情况的squid为例。我们可以在squid/src/comm.c里看到comm_openex()
是如何发现超出限制的,嗯,可以说没有”自己发现”,直接判断socket()是否成功而已。所以接下来的事情是看socket创建过程怎么判断的。
关于socket过程,主要看kernel/net/socket.c和kernel/fs/file.c,作为C菜鸟,如下系列博文在这方面说的非常清楚,我就不详细说了:
TCP/IP源码学习(47)——socket与VFS的关联(1)
TCP/IP源码学习(48)——socket与VFS的关联(2)
大体来说,就是socket本身是不触及这个限制的,但是创建出来的socket必须和文件描述符关联起来(sock_map_fd()
– sock_alloc_file()
– get_unused_fd_flags()
– alloc_fd()
),这就相关了。在alloc_fd()
中会读取当前进程(current)的fdtable,fdtable的结构由”linux/fdtable.h”定义如下:
struct fdtable {
unsigned int max_fds;
struct file ** fd; /* current fd array */
fd_set *close_on_exec;
fd_set *open_fds;
struct rcu_head rcu;
struct fdtable *next;
};
struct files_struct {
atomic_t count;
struct fdtable *fdt;
struct fdtable fdtab;
spinlock_t file_lock ____cacheline_aligned_in_smp;
int next_fd;
struct embedded_fd_set close_on_exec_init;
struct embedded_fd_set open_fds_init;
struct file * fd_array[NR_OPEN_DEFAULT];
};
#define files_fdtable(files) (rcu_dereference((files)->fdt))
打开fd的过程如下:
那么涉及max open files的显然就是这个expand_files()
了。继续看,可以发现其中的判断分三部分:
都逃过之后才进入expand_fdtable()
真正扩展。很好,现在我们看到之前就知道的ulimit/sysctl神马的是怎么限定的了。那么这第二个呢?我们可以找到files这个结构的init,如下:
struct files_struct init_files = {
.count = ATOMIC_INIT(1),
.fdt = &init_files.fdtab,
.fdtab = {
.max_fds = NR_OPEN_DEFAULT,
.fd = &init_files.fd_array[0],
.close_on_exec = (fd_set *)&init_files.close_on_exec_init,
.open_fds = (fd_set *)&init_files.open_fds_init,
.rcu = RCU_HEAD_INIT,
},
.file_lock = __SPIN_LOCK_UNLOCKED(init_task.file_lock),
};
这个NR_OPEN_DEFAULT
可以在fdtable.h里看到就是BITS_PER_LONG
。BITS_PER_LONG
应该是32或者64,取决于CPU是32还是64位的了。
其实这里还可以继续看alloc_fdtable()
中怎么确定新扩展的fdt->max_fds
的,如果nr大于sysctl的设定,那么nr会计算成
((sysctl_nr_open - 1) | (BITS_PER_LONG - 1)) + 1
然后copy_fdtable()
转移数据,奇怪的是看到转移完后,还判断了原有fdt->max_fds > NR_OPEN_DEFAULT
才释放,我不清楚什么情况下会有fdt->max_fds
小于init值了…
回到主题。三个条件里后两个条件都很明白了。现在就是第一个,这是根据current不同有不同的,我们可以试试看如果修改这个值会怎么样?
修改工具我用到了systemtap大神器,不过我是菜鸟啦~脚本如下:
#!/usr/bin/stap
%{
#include <linux/sched.h>
#include <linux/resource.h>
%}
probe begin {
printf("begin...\n")
}
probe kernel.function("expand_files@fs/file.c").call
{
if ( execname() == "squid" ) {
printf("[%s] %s fdt:%d, task:%d, rlim:%d\n", tz_ctime(gettimeofday_s()), execname(), $files->fdtab->max_fds, task_open_file_handles(task_current()), rlim_cur());
printf("\targs_nr: %d\n", $nr);
if ( rlim_cur() < $1 ) {
printf("\tset rlim: %d\n", set_rlim_cur($1));
exit();
}
}
}
probe kernel.function("expand_files@fs/file.c").return
{
if ( execname() == "squid" ) {
printf("\treturn: %d\n", $return);
}
}
probe kernel.function("expand_fdtable")
{
printf("%s call fdtable with %s", execname(), $$vars);
}
function rlim_cur:long ()
%{ /* pure */ /* unprivileged */
struct signal_struct *ss = kread( &(current->signal) );
THIS->__retvalue = kread (&(ss->rlim[RLIMIT_NOFILE].rlim_cur));
CATCH_DEREF_FAULT();
%}
function set_rlim_cur:long (val:long)
%{ /* pure */ /* unprivileged */
struct signal_struct *ss = kread( &(current->signal) );
kwrite(&(ss->rlim[RLIMIT_NOFILE].rlim_cur), THIS->val);
CATCH_DEREF_FAULT();
%}
systemtap自己提供了一系列tapset函数,比如这里的execname(),task_*都是。注意systemtap是脚本语言的,所以这些函数直接在/usr/share/systemtap/下面可以看怎么写的。比如我上面定义的两个function就是仿照里面task_max_file_handles()
写的。
用%和{}标记的是内嵌C代码,systemtap在编译成C的时候直接插入进去,可以stap -k保留在/tmp/stap123456下查看的到。
kread/kwrite是systemtap-runtime提供的函数,封装的是put_user/get_user指令。
现在我们启动squid进程和stap脚本:
ulimit -HSn 256;squid -D
stap -g max_fds.stp 1024
注意要加-g,否则不会加载内嵌C的。 另开窗口发起一次请求,然后看到stap输出:
begin...
[Fri Oct 26 19:20:34 2012 CST] squid fdt:64, task:16, rlim:256
args_nr: 12
set rlim: 0
额,这个set是返回值0。function里如果把kwrite的返回值赋给THIS->retvalue
会报void的错误。挺奇怪的。
再运行stap,发起请求,就可以看到squid的rlim变成1024了:
begin...
[Fri Oct 26 19:24:40 2012 CST] squid fdt:64, task:16, rlim:1024
args_nr: 12
return: 0
[Fri Oct 26 19:24:40 2012 CST] squid fdt:64, task:17, rlim:1024
args_nr: 17
return: 0
不过我的虚拟机跑不出这么大的并发,没法超过BITS_PER_LONG
的64。所以我们可以换一个思路来验证expand_files()
的执行。
这个时候,搞笑而现实的事情发生了。nr越过了1024的判断,却越不过BITS_PER_LONG
的判断,于是expand_files
永远过不去。squid的max open files只能停留在16。这一步的return是0。所以看到stap里.return{}打印的是0。
这个时候,由之前的测试可以看到,squid本身就要用掉十多个fd来维护运行的。所以基本一接请求nr就超过16了。squid完全无法响应请求。这一步的return是-EMFILE,于是屏幕上开始出现一行行squid call fdtable {.n}………
附注:使用systemtap需要debuginfo。内核调试需要kernel-debuginfo/kernel-devel,程序也需要。如果是自己编译的,没问题直接probe process(“${path}/command”)即可,如果是rpm安装的,那就必须得把debuginfo包安装上,比如nginx-debuginfo.rpm这样子。
2012年12月3日更新:
采用tc延时的办法,可以达到在虚拟机上获取squid高连接的模拟环境。然后作出如下修正:
rlim_cur
的时候也需要修改rlim_max
在上面的缩小测试里不会触发问题,不过在增大的测试中,问题来了——setrlimit()
函数会很诧异为毛自己参数里那个&rl
的rlim_cur
比rlim_max
还大?然后悲剧的报出”etrlimit(RLIMIT_NOFILE) failed: Invalid argument (22)”的错误……
kwrite(&(ss->rlim[RLIMIT_NOFILE].rlim_max), THIS->val);
Squid_MaxFD
全局变量上面都是对kernel里的socket和file的修改,在实际运用中,用户程序本身也会有各种判断。squid维护了一堆全局变量,比如Squid_MaxFD
,Biggest_FD
和Number_FD
,这就是squidclient mgr:info里看到的关于文件描述符的那几个值。其中Squid_MaxFD
是在init的时候根据主进程启动时的ulimit情况一次性设定的,即便child进程重启也不会变。而Biggest_FD
则是由fdUpdateBiggest()
函数每次更新。不巧的是,里面有这么一句判断:
assert(fd < Squid_MaxFD);
所以,光修改kernel里的限制,socket返回后在更新Biggest_FD
时squid会直接挂掉……
下面是修改squid进程里全局变量的办法,和修改kernel其实很类似:
probe process("/usr/sbin/squid").function("fdUpdateBiggest@src/fd.c")
{
if ( $Squid_MaxFD < 65535 ) {
$Squid_MaxFD = 65535;
}
}
把上面两个修改加入到之前的文件,然后测试增大ulimit限制(记住ulimit要大于64,否则不起作用哟),就没问题了。