刨根問底,看我如何處理 Too many open files 錯誤!
如果你的項目中支持高并發(fā),或者是測試過比較多的并發(fā)連接。那么相信你一定遇到過“Too many open files”這個錯誤。
這個錯誤的出現(xiàn)其實是正常的,因為每打開一個文件(包括socket),都需要消耗一定的內(nèi)存資源。為了避免個別進程不受控制地打開了過多的文件而讓整個服務器崩潰,Linux 對打開的文件描述符數(shù)量有限制。
但是解決這個錯誤“奇葩”的地方在于,竟然需要修改三個參數(shù):fs.nr_open、nofile(其實 nofile 還分 soft 和 hard) 和 fs.file-max。這幾個參數(shù)里有的是進程級的、有的是系統(tǒng)級的、有的是用戶進程級的,說一遍都覺得好亂。而且另外這幾個參數(shù)還有依賴關系,著實比較復雜。
不知道你,反正飛哥我是根本記不住哪個是哪個。每次遇到這種問題,還是都得再繼續(xù) Google 一遍。但由于復雜性,所以其實網(wǎng)上的很多帖子里也都并沒有真正搞清楚。如果照搜索出來的文章改,稍有不慎就會踩雷,把機器搞出問題。
我在測試最大TCP連接數(shù)的時候就踩過兩次坑。
第一次是當時開了二十個子進程,每個子進程開啟了五萬個并發(fā)連接興高采烈準備測試百萬并發(fā)。結果倒霉催地忘了改 file-max 了。實驗剛開始沒多大一會兒就開始報錯“Too many open files”。但問題是這個時候更悲催的是發(fā)現(xiàn)所有的命令包括 ps、kill也同時無法使用了。因為它們也都需要打開文件才能工作。后來沒辦法,重啟系統(tǒng)解決的。
另外一次是重啟機器完了之后發(fā)現(xiàn)無法 ssh 登錄了。后來找運維工程部的同學報障以后才算是修復。最終發(fā)現(xiàn)是因為 hard nofile 比 fs.nr_open 高了,直接導致無法登陸。(其實我把 fs.nr_open 加大過,但是用的是 echo 命令 修改的。系統(tǒng)一重啟,還原了)。
一、找到源代碼
對于這三個家伙,我真的是無法言語更多了。所以我下定了決心,一定要把它們徹底搞清楚。怎么搞?那沒有比把它的源碼扒出來能看的更準確了。我們就拿創(chuàng)建 socket 來舉例,首先找到 socket 系統(tǒng)調(diào)用的入口
//file:?net/socket.c
SYSCALL_DEFINE3(socket,?int,?family,?int,?type,?int,?protocol)
{
?retval?=?sock_map_fd(sock,?flags?&?(O_CLOEXEC?|?O_NONBLOCK));
?if?(retval?0)
??goto?out_release;
}
我們看到 socket 調(diào)用 sock_map_fd 來創(chuàng)建相關內(nèi)核對象。接著我們再進入 sock_map_fd 瞧瞧。
//file:?net/socket.c
static?int?sock_map_fd(struct?socket?*sock,?int?flags)
{
?struct?file?*newfile;
?//在這里會判斷打開文件數(shù)是否超過?soft?nofile?和?fs.nr_open
?//獲取?fd?句柄號
?int?fd?=?get_unused_fd_flags(flags);??
?if?(unlikely(fd?0))
??return?fd;
?//在這里會判斷打開文件數(shù)是否超過?fs.file-max
?//創(chuàng)建?sock_alloc_file對象
?newfile?=?sock_alloc_file(sock,?flags,?NULL);?
?if?(likely(!IS_ERR(newfile)))?{
??fd_install(fd,?newfile);
??return?fd;
?}
?put_unused_fd(fd);
?return?PTR_ERR(newfile);
}
為什么創(chuàng)建一個socket又要申請 fd,又要申請 sock_alloc_file 呢?我們看一個進程打開文件時的內(nèi)核數(shù)據(jù)結構圖就明白了

結合上圖,就能輕松理解這兩個函數(shù)的作用
get_unused_fd_flags:申請 fd,這只是一個在找一個可用的數(shù)組下標而已 sock_alloc_file:申請真正的 file 內(nèi)核對象
二、找到進程級限制 nofile 和 fs.nr_open
接下來,我們再回到最大文件數(shù)量的判斷上。這里我直接把結論拋出來。get_unused_fd_flags 中判斷了 nofile、和 fs.nr_open。如果超過了這兩個參數(shù),就會報錯。請看!
//file:?fs/file.c
int?get_unused_fd_flags(unsigned?flags)
{
?//?RLIMIT_NOFILE?是?limits.conf?中配置的?nofile
?return?__alloc_fd(
??current->files,?
??0,?
??rlimit(RLIMIT_NOFILE),?
??flags
?);
}
在get_unused_fd_flags 中,調(diào)用了 rlimit(RLIMIT_NOFILE)。這個是讀取的 limits.conf 中配置的 soft nofile,代碼如下:
//file:?include/linux/sched.h
static?inline?unsigned?long?task_rlimit(const?struct?task_struct?*tsk,
??unsigned?int?limit)
{
?return?ACCESS_ONCE(tsk->signal->rlim[limit].rlim_cur);
}
通過當前進程描述訪問到 rlim[RLIMIT_NOFILE],這個對象的 rlim_cur 是 soft nofile(rlim_max 對應 hard nofile )。
緊接著讓我們進入 __alloc_fd() 中來
//file:?include/uapi/asm-generic/errno-base.h
#define?EMFILE??24?/*?Too?many?open?files?*/
int?__alloc_fd(struct?files_struct?*files,
????????unsigned?start,?unsigned?end,?unsigned?flags)
{
?...
?error?=?-EMFILE;
?//看要分配的文件號是否超過?end(limits.conf?中的?nofile)
?if?(fd?>=?end)
??goto?out;
?error?=?expand_files(files,?fd);
?if?(error?0)
??goto?out;
?...
}
在__alloc_fd() 中會判斷要分配的句柄號是不是超過了 limits.conf 中 nofile 的限制。fd 是當前進程相關的,是一個從 0 開始的整數(shù)。如果超限,就報錯 EMFILE (Too many open files)。
這里注意個小細節(jié),那就是進程里的 fd 是一個從 0 開始的整數(shù)。只要確保分配出去的 fd 編號不超過 limits.conf 中 nofile,就能保證該進程打開的文件總數(shù)不會超過這個數(shù)。
接著我們看到調(diào)用又會進入 expand_files:
static?int?expand_files(struct?files_struct?*files,?int?nr)
{
?//2.?判斷打開文件數(shù)是否超過?fs.nr_open
?if?(nr?>=?sysctl_nr_open)???
??return?-EMFILE;
}
在 expand_files 我們看到,又到 nr (就是 fd 編號) 和 fs.nr_open 相比較了。超過這個限制,返回錯誤 EMFILE (Too many open files)。
由上可見,無論是和 fs.nr_open,還是和 soft nofile 比較,都用的是當前進程的文件描述符序號在比較的,所以這兩個參數(shù)都是進程級別的。
有意思的是和這兩個參數(shù)的比較幾乎是前后腳進行的,所以它兩的作用也基本一樣。Linux之所以分兩個參數(shù)來控制,那是因為 fs.nr_open 是系統(tǒng)全局的,而 nofile 則可以分用戶來分別控制。
所以,現(xiàn)在我們可以得出第一個結論。
結論1:soft nofile 和 fs.nr_open的作用一樣,它兩都是限制的單個進程的最大文件數(shù)量。區(qū)別是 soft nofile 可以按用戶來配置,而 fs.nr_open 所有用戶只能配一個。
三、找到系統(tǒng)級限制 fs.nr_open
我們在回過頭來看 sock_map_fd 中調(diào)用的另外一個函數(shù) sock_alloc_file,在這個函數(shù)里我們發(fā)現(xiàn)它會和 fs.file-max 這個系統(tǒng)參數(shù)來比較。用啥比的呢?
//file:?fs/file_table.c
struct?file?*sock_alloc_file(struct?socket?*sock,?int?flags,?const?char?*dname)
{
?file?=?alloc_file(&path,?FMODE_READ?|?FMODE_WRITE,
???&socket_file_ops);
}
struct?file?*alloc_file(struct?path?*path,?fmode_t?mode,
??const?struct?file_operations?*fop)
{
?file?=?get_empty_filp();
?...
}
struct?file?*get_empty_filp(void)
{
?//files_stat.max_files就是?fs.file-max參數(shù)
?if?(get_nr_files()?>=?files_stat.max_files?
??&&?!capable(CAP_SYS_ADMIN)?//注意這里root賬號并不受限制
??)?{
?}
}
可見是用 get_nr_files() 來和 fs.file-max來比較的。根據(jù)該函數(shù)的注釋我們能看到它是當前系統(tǒng)打開的文件描述符總量。如下:
/*
?*?Return?the?total?number?of?open?files?in?the?system
?*/
static?long?get_nr_files(void)
{
?...
另外注意下 !capable(CAP_SYS_ADMIN) 這行。看完這句,我才恍然大悟,原來 file-max 這個參數(shù)只限制非 root 用戶。開篇中我提到的文件打開過多時無法使用 ps,kill 等命令,是因為我用的非 root 賬號操作的。哎,下次再遇到這種文件直接用 root 去 kill 就行了。之前竟然丟臉地采用了重啟機器大法。。
所以現(xiàn)在我們可以得出另一個結論了。
結論2:fs.file-max: 整個系統(tǒng)上可打開的最大文件數(shù),但不限制 root 用戶
總結一下
我們總結一下,其實在 Linux 上能打開多少個文件,限制有兩種:
第一種,進程級別的,限制的是單個進程上可打開的文件數(shù)。具體參數(shù)是 soft nofile 和 fs.nr_open。它們兩個的區(qū)別是 soft nofile 可以不同用戶配置不同的值。而 fs.nr_open 在一臺 Linux 上只能配一次。
第二種,系統(tǒng)級別的,整個系統(tǒng)上可打開的最大文件數(shù),具體參數(shù)是fs.file-max。但是這個參數(shù)不限制 root 用戶。
另外這幾個參數(shù)之間還有耦合關系,因此還要注意以下三點:
1、如果你想加大 soft nofile, ?那么 hard nofile 也需要一起調(diào)整。因為如果 hard nofile 設置的低, 你的 soft nofile 設置的再高都沒用,實際生效的值會按二者里最低的來。
2、如果你加大了 hard nofile,那么 fs.nr_open 也都需要跟著一起調(diào)整。如果不小心把 hard nofile 設置的比 fs.nr_open 大了,后果比較嚴重。會導致該用戶無法登陸。如果設置的是 * 的話,那么所有的用戶都無法登陸。
3、還要注意如果你加大了 fs.nr_open,但是用的是 echo "xx" > ../fs/nr_open 的方式,剛改完你可能覺得沒問題。只要機器一重啟你的 fs.nr_open 設置就會失效,還是會無法登陸。
假如你想讓你的進程可以打開 100 萬個文件描述符,我覺得比較穩(wěn)妥點的修改方法是干脆都直接用 conf 文件的方式來改。這樣比較統(tǒng)一,也比較安全。
#?vi?/etc/sysctl.conf
fs.nr_open=1100000??//要比?hard?nofile?大一點
fs.file-max=1100000?//多留點buffer
#?sysctl?-p
#?vi?/etc/security/limits.conf
*??soft??nofile??1000000
*??hard??nofile??1000000
通過這種方式修改,你就可以繞過飛哥踩過的坑了。
