本文转载自微信公众号《内功修炼养成》,作者张燕飞allen。转载本文请联系内功修炼发展公众号。如果你的项目支持高并发,或者测试了更多的并发连接数。那么相信你一定遇到过“Toomanyopenfiles”的错误。出现这个错误其实是正常的,因为每次打开一个文件(包括socket),都需要消耗一定的内存资源。为了防止个别进程不受控制地打开过多的文件而导致整个服务器崩溃,Linux对打开的文件描述符数量进行了限制。但是解决这个错误的“妙处”在于需要修改三个参数:fs.nr_open、nofile(其实nofile也分soft和hard)和fs.file-max。这些参数有的是进程级的,有的是系统级的,有的是用户进程级的。另外,这些参数还有依赖关系,确实很复杂。不知道你怎么样,反正飞哥我也记不清哪个是哪个了。每次遇到这种问题,还是得继续Google一遍。不过由于比较复杂,其实网上很多帖子都没有真正想明白。如果按照搜索到的文章改,一不小心就会踩到地雷,导致机器出现问题。我在测试最大TCP连接数的时候踩了两次坑。第一次开了20个子进程,每个子进程开了5万个并发连接,兴高采烈准备测试百万并发。结果,不幸的是,我忘了更改file-max。实验开始没多久,就开始报错“Toomanyopenfiles”。但是问题是,这个时候,更惨的是包括ps、kill在内的所有命令也同时不可用了。因为他们都需要打开文件才能工作。后来也没有办法通过重启系统解决。还有一次是重启机器后,发现无法ssh登录。后来让运维工程部的同学报了故障,才修好。最后发现hardnofile比fs.nr_open高,直接导致无法登录。(其实我是把fs.nr_open放大了,但是用echo命令修改了。一旦系统重启,它将被恢复)。1.找源码对于这三个家伙,我真的是说不完。于是我下定决心,要把他们彻底弄清楚。怎么做?没有什么比挖掘它的源代码更准确的了。我们以创建socket为例,首先找到socket系统调用的入口//file:net/socket.cSYSCALL_DEFINE3(socket,int,family,int,type,int,protocol){retval=sock_map_fd(sock,flags&(O_CLOEXEC|O_NONBLOCK));if(retval<0)gotoout_release;}我们看到socket调用sock_map_fd创建了相关的内核对象。然后我们输入sock_map_fd看看。//file:net/socket.cstaticintsock_map_fd(structsocket*sock,intflags){structfile*newfile;//这里会判断打开文件数是否超过softnofile和fs.nr_open//获取fd句柄数intfd=get_unused_fd_flags(flags);if(unlikely(fd<0))returnfd;//这里会判断打开文件数是否超过fs.file-max//创建sock_alloc_file对象newfile=sock_alloc_file(sock,flags,NULL);if(likely(!IS_ERR(newfile))){fd_install(fd,newfile);returnfd;}put_unused_fd(fd);returnPTR_ERR(newfile);}为什么创建socket需要申请fd和sock_alloc_file?再看看进程打开文件时的内核数据结构图一目了然结合上图,可以很容易理解这两个函数的作用申请一个真正的文件内核对象2.找到进程级别Limitnofile和fs.nr_open接下来,我们回到最大文件数的判断。这里我直接抛出结论。nofile,fs.nr_open在get_unused_fd_flags中判断。如果超过这两个参数,就会报错。请看!//file:fs/file.cintget_unused_fd_flags(unsignedflags){//RLIMIT_NOFILE是limits.conf中配置的nofilereturn__alloc_fd(current->files,0,rlimit(RLIMIT_NOFILE),flags);}在get_unused_fd_flags中,调用rlimit(RLIMIT_NOFILE)。这是读limits.conf中配置的软nofile,代码如下:.rlim_cur);}通过当前进程描述访问rlim[RLIMIT_NOFILE],该对象的rlim_cur为softnofile(rlim_max对应hardnofile)。然后让我们输入__alloc_fd()//file:include/uapi/asm-generic/errno-base.h#defineEMFILE24/*Toomanyopenfiles*/int__alloc_fd(structfiles_struct*files,unsignedstart,unsignedend,unsignedflags){...error=-EMFILE;//查看要分配的文件数是否超过end(limits.conf中的nofile)if(fd>=end)gotoout;error=expand_files(files,fd);if(error<0)gotoout;...}在__alloc_fd()中判断分配的句柄数是否超过limits.conf中nofile的限制。fd是相对于当前进程的,是一个从0开始的整数,如果超过限制,会报错EMFILE(Toomanyopenfiles)。这里注意一个小细节,就是进程中的fd是一个从0开始的整数,只要保证分配的fd个数不超过limits.conf中的nofile,就可以保证总的fd个数进程打开的文件不会超过此数量。然后我们看到调用会再次进入expand_files:staticintexpand_files(structfiles_struct*files,intnr){//2。判断打开文件数是否超过fs.nr_openif(nr>=sysctl_nr_open)return-EMFILE;}在expand_files中我们看到,将nr(即fd数)与fs.nr_open进行比较。超过此限制会返回错误EMFILE(打开的文件过多)。从上面可以看出,不管是和fs.nr_open还是softnofile比较,都是用当前进程的文件描述符号来比较的,所以这两个参数都是进程级别的。有意思的是这两个参数的比较几乎是前后做的,所以两个函数基本一样。Linux之所以由两个参数控制,是因为fs.nr_open对系统是全局的,而nofile则可以由用户单独控制。所以,现在我们可以得出第一个结论。结论1:softnofile和fs.nr_open的作用一样,都是限制单个进程的最大文件数。不同的是softnofile可以为每个用户配置,而fs.nr_open只能为所有用户配置一个。3.找到系统级限制fs.nr_open我们再回头看看sock_map_fd中的另一个函数sock_alloc_file。在这个函数中,我们发现它会和系统参数fs.file-max进行比较。用什么?//file:fs/file_table.cstructfile*sock_alloc_file(structsocket*sock,intflags,constchar*dname){file=alloc_file(&path,FMODE_READ|FMODE_WRITE,&socket_file_ops);}structfile*alloc_file(structpath*path,fmode_tmode,conststructfile_operations*fop){file=get_empty_filp();...}structfile*get_empty_filp(void){//files_stat.max_files为fs.file-max参数if(get_nr_files()>=files_stat.max_files&&!capable(CAP_SYS_ADMIN)//注意这里不限制root账号){}}可以看出get_nr_files()是用来和fs.file-max比较的。根据这个函数的注释可以看出,它是当前系统打开的文件描述符总数。如下:/**Returnthetotalnumberofopenfilesinthesystem*/staticlongget_nr_files(void){...另外注意!capable(CAP_SYS_ADMIN)这一行。看完这句话,恍然大悟,原来file-max参数只限制非root用户。开头提到了打开文件太多的时候不能用ps,kill等命令,因为我是用非root账号操作的。哎,下次再遇到这种文件,用root杀掉就好了。之前,我可耻地采用了重启机器的方法。.所以现在我们可以得出另一个结论。结论二:fs.file-max:整个系统可以打开的最大文件数,但不限制root用户。让我们总结一下。其实在Linux上对打开多少文件有两个限制:第一个是进程级别,限制是单个进程可以打开的文件数。具体参数是softnofile和fs.nr_open。它们的区别在于softnofile可以为不同的用户配置不同的值。fs.nr_open只能在Linux上配置一次。第二种是系统级别的,整个系统上可以打开的最大文件数,具体参数是fs.file-max。但是这个参数不限制root用户。另外,这些参数之间存在耦合关系,所以要注意以下三点:1、如果要增加softnofile,那么hardnofile也需要一起调整。因为如果hardnofile设置低了,你的softnofile设置再高也没用,实际有效值会是两者中最低的。2.如果增加了hardnofile,那么fs.nr_open也需要一起调整。如果不小心把hardnofile设置得比fs.nr_open大,后果很严重。这样会导致用户无法登录,如果设置了*,那么所有用户都无法登录。3.另外注意如果增加fs.nr_open的大小,但是使用echo"xx">../fs/nr_open方法,你可能觉得修改后就ok了。只要机器重启,你的fs.nr_open设置就会失效,你仍然无法登录。如果你希望你的进程能够打开100万个文件描述符,我认为更安全的修改方式是只需使用conf文件来修改它们。这样更均匀也更安全。#vi/etc/sysctl.confs.nr_open=1100000//比hardnofile大一点fs.file-max=1100000//多留一些buffer#sysctl-p#vi/etc/security/limits.conf*softnofile1000000*hardnofile1000000passed这样修改,就可以绕过飞哥踩过的坑了。
