linux檔案描述符限制和單機最大長連接配接數
相關參數
linux系統中與檔案描述符相關的參數有以下幾個:
-
soft/hard nofile
-
file-max(/proc/sys/fs/file-max)
-
nr_open(/proc/sys/fs/nr_open)
這三個參數的作用都是限制一個程序可以打開的最大檔案數,它們之間有什麼差別和聯系呢?本文會從核心代碼出發,分析這些參數是怎麼影響檔案打開的,以及它們之間什麼差別,并在最後讨論一個于此有關的流行話題:單機最大長連接配接數。
如果你沒有興趣看核心代碼,請直接跳到“檔案描述符總結”處。
核心剖析
(本文中引用的核心是linux-2.6.39的代碼,用于測試的作業系統為centos7 Linux 3.10.0-229.el7.x86_64)
當使用者調用open時,産生中斷進入核心态,核心接着調用這個函數
do_sys_open
:
long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
struct open_flags op;
int lookup = build_open_flags(flags, mode, &op); // 根據傳入的參數構造open_flags結構
char *tmp = getname(filename); // 對檔案名做長度檢測
int fd = PTR_ERR(tmp);
if (!IS_ERR(tmp)) {
fd = get_unused_fd_flags(flags); // 根據flags擷取一個空閑的fd,此處為關鍵之一
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op, lookup); // 打開指定檔案,并和之前擷取的fd關聯,此處為關鍵之二
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
fd_install(fd, f);
}
}
putname(tmp);
}
return fd;
}
get_unused_fd_flags
#define get_unused_fd_flags(flags) alloc_fd(0, (flags))
alloc_fd
int alloc_fd(unsigned start, unsigned flags)
{
struct files_struct *files = current->files;// current指向目前程序的結構,而current->files管理着本程序所有打開的檔案
unsigned int fd;
int error;
struct fdtable *fdt;
spin_lock(&files->file_lock);
repeat:
fdt = files_fdtable(files); // 擷取檔案描述表,由此可知每個程序都維護一張檔案描述符表,這張表的大小限制着本程序可以打開的檔案數
fd = start;
if (fd < files->next_fd)
fd = files->next_fd;
if (fd < fdt->max_fds) // 先嘗試在現有表範圍内尋找未使用的fd,fdt->max_fds是檔案描述符表裡最大的fd
fd = find_next_zero_bit(fdt->open_fds->fds_bits,
fdt->max_fds, fd);
// 擴充檔案描述表, 僅當現有表沒有未使用的fd時才擴充
error = expand_files(files, fd);
if (error < 0)
goto out;
...
}
int expand_files(struct files_struct *files, int nr)
{
struct fdtable *fdt;
fdt = files_fdtable(files);
/*
* N.B. For clone tasks sharing a files structure, this test
* will limit the total number of files that can be opened.
*/
// rlimit(RLIMIT_NOFILE)的作用是擷取soft nofile,當新的fd大于soft nofile時傳回-EMFILE,EMFILE對應我們通常看到的錯誤資訊“Too many open files”
if (nr >= rlimit(RLIMIT_NOFILE))
return -EMFILE;
/* Do we need to expand? */
if (nr < fdt->max_fds) // 此處判斷是否需要擴充檔案描述符表
return 0;
/* Can we expand? */
if (nr >= sysctl_nr_open) // sysctl_nr_open即/proc/sys/fs/nr_open。當fd大于nr_open時傳回EMFILE
return -EMFILE;
/* All good, so we try */
return expand_fdtable(files, nr);
}
從上面代碼可以看到,當fd大于
soft nofile
或者大于
nr_open
時,傳回EMFILE。
soft nofile
和
nr_open
都限制了fd的申請,差別在于,
nr_open
的判斷是在需要擴充fdtable的時候,是以可以了解為
soft nofile
直接限制打開fd的數目,而
nr_open
限制了fdtable的擴充(間接限制了fd的打開數目)。
另一點差別是
soft nofile
是程序内部的參數,修改它不影響其他程序,而
nr_open
是作業系統參數,修改它影響到系統的所有程序。上述代碼裡面的rlimit(RLIMIT_NOFILE)實際上是調用的
current->signal->rlim[limit].rlim_cur
,current是目前程序的結構指針,是以soft nofile是程序級别的,而
sysctl_nr_open
是核心全局變量,定義在file.c裡面:
int sysctl_nr_open __read_mostly = 1024*1024
,這個值可以通過sysctl來修改。
核心在限制fd時沒有用打開的fd數目來比較(核心也沒有專門的變量來記錄打開的fd數目)而是用fd直接比較,因為fd的申請是按從小到大順序的,是以用fd來做數目的比較可以達到相同的效果。
至此,我們已經看到了兩個參數
soft nofile
和
nr_open
它們是在fd的申請過程中起作用,再來看下檔案的打開過程:
struct file *do_filp_open(int dfd, const char *pathname,
const struct open_flags *op, int flags)
{
struct nameidata nd;
struct file *filp;
// 實際的打開函數path_openat
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(dfd, pathname, &nd, op, flags);
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
return filp;
}
do_filp_open
做了一層封裝,真正起作用的是
path_openat
,根據路徑來打開檔案,并傳回file指針:
static struct file *path_openat(int dfd, const char *pathname,
struct nameidata *nd, const struct open_flags *op, int flags)
{
struct file *base = NULL;
struct file *filp;
struct path path;
int error;
filp = get_empty_filp(); // 擷取空閑file指針,此處為關鍵
if (!filp)
return ERR_PTR(-ENFILE);
filp->f_flags = op->open_flag;
nd->intent.open.file = filp;
nd->intent.open.flags = open_to_namei_flags(op->open_flag);
nd->intent.open.create_mode = op->mode;
error = path_init(dfd, pathname, flags | LOOKUP_PARENT, nd, &base);
...
}
類似于fd的申請,檔案打開過程也有相似的函數
get_empty_filp()
,擷取空閑指針,進入這個函數看看:
struct file *get_empty_filp(void)
{
const struct cred *cred = current_cred();
static long old_max;
struct file * f;
/*
* Privileged users can go above max_files
*/
// get_nr_files擷取目前打開的檔案數,而files_stat.max_files對應file-max參數,可以在sysctl的代碼裡面找到
// 下面這段代碼的意思是當目前程序沒有CAP_SYS_ADMIN權限的時候,才會比較file-max,
// 而root使用者預設是有CAP_SYS_ADMIN權限的,這意味着當程式以root使用者啟動時,可打開的檔案數不受file-max限制。
if (get_nr_files() >= files_stat.max_files && !capable(CAP_SYS_ADMIN)) {
/*
* percpu_counters are inaccurate. Do an expensive check before
* we go and fail.
*/
// 更精确的計算打開的檔案數:所有cpu計數累加,這種操作有資料同步問題,更耗時,是以先用本cpu計數做判斷。
if (percpu_counter_sum_positive(&nr_files) >= files_stat.max_files)
goto over;
}
f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);
if (f == NULL)
goto fail;
// 檔案打開,計數+1
// 核心percpu counter機制,優化性能
percpu_counter_inc(&nr_files);
...
return f;
over:
/* Ran out of filps - report that */
// 當系統打開的fd超過file-max時,在log裡面看到的錯誤資訊就是從這裡輸出的。
// old_max的作用是僅當fd超标且一直在增加的情況下才會輸出日志,減少的情況則不報日志,畢竟情況在變好嘛。
if (get_nr_files() > old_max) {
pr_info("VFS: file-max limit %lu reached\n", get_max_files());
old_max = get_nr_files();
}
goto fail;
fail_sec:
file_free(f);
fail:
return NULL;
}
從上面的代碼和注釋可以看出file-max是用來限制file結構的建立的,但當程序擁有
CAP_SYS_ADMIN
權限的時候可以突破file-max限制(man 7
capabilities),在實際應用中表現為root啟動的程序不受file-max限制,也就是說nofile和nr_open有可能超過file-max,這種情況下,程序可打開的檔案數就會超越file-max,這将導緻其他非root啟動的程序資源耗盡。
到這裡一直沒有看到hard nofile,hard nofile沒有參與檔案的打開過程,它的作用僅僅是限制soft nofile的大小,看下setrlimit的代碼:
int do_prlimit(struct task_struct *tsk, unsigned int resource,
struct rlimit *new_rlim, struct rlimit *old_rlim)
{
struct rlimit *rlim;
int retval = 0;
if (resource >= RLIM_NLIMITS)
return -EINVAL;
if (new_rlim) {
// rlim_cur是soft nofile,rlim_max是hard nofile,設定的soft nofile必須小于hard nofile,否則傳回失敗
if (new_rlim->rlim_cur > new_rlim->rlim_max)
return -EINVAL;
// 下面這裡是個意外收獲,nr_open竟然和hard nofile有關系,而且hard nofile不能超過nr_open,否則傳回“Operation not permitted”
if (resource == RLIMIT_NOFILE &&
new_rlim->rlim_max > sysctl_nr_open)
return -EPERM;
}
...
// 擷取目前程序的rlimit
rlim = tsk->signal->rlim + resource;
task_lock(tsk->group_leader);
if (new_rlim) {
// 如果目前程序沒有CAP_SYS_RESOURCE權限,則禁止設定的hard nofile超過現在的值
// 在實際應用中表現為非root使用者無權提升hard nofile的值
if (new_rlim->rlim_max > rlim->rlim_max &&
!capable(CAP_SYS_RESOURCE))
retval = -EPERM;
if (!retval)
retval = security_task_setrlimit(tsk->group_leader,
resource, new_rlim);
...
}
...
}
通過上面的代碼,可以知道hard nofile的作用僅僅是限制soft nofile的大小,當我們調用setrlimit設定nofile的時候,如果傳入的soft nofile大于hard nofile則會傳回失敗。這樣聽起來似乎hard nofile沒什麼用因為很多時候我們都習慣于一起把soft nofile和hard nofile設定成一個值,但是比較安全的做法是隻修改soft nofile,非必須不要修改hard nofile,改小一般不是我們想要的,但是改大hard nofile甚至超過nr_open很容易造成系統資源耗盡,導緻其他程序和系統故障。最好先用getrlimit擷取現在的hard nofile,然後在setrlimit時傳入這個值,可以保證不修改hard nofile。
nofile、nr_open、file-max分别可以改多大?
上面提到nofile受限于
nr_open
,那麼就先來看下
nr_open
,它的資料類型是int,理論上應該能達到2147483647,這個是int類型的最大值。
在設定nr_open之前先看下系統預設值:
[xuwei@localhost ~]$ cat /proc/sys/fs/nr_open
1048576
1048576正好是1024*1024,跟核心代碼裡面的預設值一樣,現在把這個值改成2147483647:
root@localhost proc]# echo 2147483647 > /proc/sys/fs/nr_open
bash: echo: write error: Invalid argument
出錯了,通過不斷減小這個值,測得的最大值為2147483584,即7FFFFFC0,也就是在MAXINT(2147483647)基礎上按64位元組對齊:
[root@localhost proc]# echo 2147483584 > /proc/sys/fs/nr_open
[root@localhost proc]# cat /proc/sys/fs/nr_open
2147483584
是以
nr_open
最大值受int類型限制成立。
再看下nofile的最大值,先改hard nofile:
[root@localhost proc]# ulimit -n 2147483584 -H
[root@localhost proc]# ulimit -n 2147483585 -H
bash: ulimit: open files: cannot modify limit: Operation not permitted
[root@localhost proc]# ulimit -n -H
2147483584
hard nofile的最大值正好等于剛才設定nr_open,符合預期,接着看下soft nofile:
[root@localhost proc]# ulimit -n 2147483584 -S
[root@localhost proc]# ulimit -n 2147483585 -S
bash: ulimit: open files: cannot modify limit: Invalid argument
[root@localhost proc]# ulimit -n -S
2147483584
soft nofile的最大值等于hard nofile。
最後是file-max,它在核心裡面是unsigned long類型的整數,理論上可以達到18446744073709551615, 下面來測試下:
[root@localhost proc]# echo 18446744073709551615 > /proc/sys/fs/file-max
[root@localhost proc]# cat /proc/sys/fs/file-max
18446744073709551615
[root@localhost proc]# echo 18446744073709551616 > /proc/sys/fs/file-max
[root@localhost proc]# cat /proc/sys/fs/file-max
0
可以修改成功,再大就回到0了,可見在設定file-max時核心沒有對邊界值做檢測。不過盡管file-max可以改設定成這高,但已經意義不大了,因為打開檔案時先比較nr_open,再比較file-max,是以實際可打開的檔案數永遠不會超過2147483584。
nofile、nr_open、file-max的大小關系
通過以上分析已經确定的是:
soft nofile <= hard nofile <= nr_open
這個是核心代碼明确限制的,尚有疑惑的是nofile和file-max以及nr_open和file-max之間的大小關系。
當nofile > file-max時:
[root@localhost ~]# cat /proc/sys/fs/file-max
1000
[root@localhost ~]# ulimit -n
2147483584
[root@localhost ~]# echo a > a.txt
雖然nofile的值大于file-max,但是隻要實際打開的檔案沒有超過file-max,即資源沒有耗盡時,是沒有任何關系的。雖然如此,nofile大于file-max是非常危險的,如果某個程序毫無節制的打開檔案就會導緻系統資源耗盡,進而導緻其他程序無資源可用,系統故障、無法登陸等嚴重問題。
當nr_open > file-max時:
[root@localhost xuwei]# cat /proc/sys/fs/file-max
97790
[root@localhost xuwei]# cat /proc/sys/fs/nr_open
1048576
[root@localhost xuwei]# echo 80000 > /proc/sys/fs/nr_open
[root@localhost xuwei]# cat /proc/sys/fs/nr_open
80000
理論和實踐證明
nr_open
和
file-max
沒有直接的大小關系。雖然
nr_open
和
file-max
都是系統級參數,但是
file-max
限制的是系統所有程序所打開的檔案的總數,它的額度是所有程序共用的,而
nr_open
隻限制單個程序可打開的檔案數,每個程序有自己獨立的
nr_open
額度。類似于
nofile
,當
nr_open
大于
file-max
時,很容易造成系統資源耗盡,導緻其他程序無法發檔案,系統無法登陸等嚴重問題。
檔案描述符總結
根據以上分析,檔案打開流程以及三個參數對其的影響如下圖所示:
檔案的打開主要分兩步,即申請fd和建立檔案結構兩個過程,
nofile
和
nr_open
在第一個過程起作用,
file-max
在第二個過程起作用。
nofile
直接限制fd的申請,
nr_open
限制檔案描述符表的擴充,間接限制了fd的申請,
file-max
限制檔案的實際建立過程。
nofile
,
nr_open
,
file-max
這三個參數的差別如下:
注:
- 系統參數的意思是,
儲存在各個程序的結構裡,nofile
和nr_open
儲存在系統變量裡。file-max
- 額度共享,
和nofile
從程序級别限制,随着檔案的打開,它們額度的減少不影響其他程序,nr_open
是系統級别的限制,一個程序多打開了一個檔案,其他程序可用的file-max
額度就少了一個。file-max
單機最大長連接配接數?
linux系統單機支援的tcp連接配接數主要受三個方面的限制:
- 檔案描述符的限制
- tcp本身的限制
- 系統記憶體限制
因為每個tcp連接配接都對應一個socket對象,而每個socket對象本身就占用一個檔案描述符,檔案描述符的限制在前文已經分析過,單機可以達到20+億,如果不考慮其他限制,單機支援的tcp長連接配接數就是20+億,這個值是非常可觀的,它絕對可以滿足世界上任何一個系統對長連接配接的需求,隻要一台機器就可以哦。
談到tcp本身的限制,就涉及到tcp四元組(遠端IP,遠端端口号,本地IP,本地端口号),它辨別一個tcp連接配接。根據常識了解,IP位址限定了一台主機(準确的說是網卡),端口号則限定了這個IP上的tcp連接配接。對于兩個tcp連接配接,四個參數中必然是有一個不同的,是以四元組的數目決定了tcp連接配接的個數。對于服務端程式來,一般來說,本地ip和本地端口号固定,是以它上面可接受的的連接配接數=2^32*65536=2^48(不考慮少量的特殊ip和特殊端口号),這也是個海量數字,基本可以支援世界上任何系統。對于用戶端程式來說,一般本地ip、遠端ip、遠端口号都是固定的,是以可以支援的長連接配接數最多隻有65536個,是以作為用戶端的tcp代理比較容易出現端口号耗盡問題。
linux系統對ip沒有限制,對端口号有限制,相關參數為
ip_local_port_range
:
[root@localhost xuwei]# cat /proc/sys/net/ipv4/ip_local_port_range
1024 65535
這兩個值分别代表最小值和最大值,小于1024的端口号一般是預留給系統使用的,這不是強制的,你一定要把最小值改成小于1024也是可以的。
這個端口号範圍參數
ip_local_port_range
對于服務端程式沒太大意義,服務端監聽端口号一般也就幾個,對于用戶端來說,比如一些tcp代理程式,或壓測用戶端,這些程式通常會建立很多連接配接,這個參數就顯得很重要。
關于系統記憶體限制,主要是兩方面,一是tcp中繼資料的大小,包含sock、inode、file等結構;二是tcp緩存占用空間,這又包含系統緩存和使用者緩存,系統緩存是系統調用read/write使用的緩存,使用者的緩存是碼農在寫代碼時設計的緩沖區,在異步服務端程式裡面用于把讀寫和資料解析處理分離。
我做過測試,寫兩個程式,服務端隻接收連接配接,用戶端隻發起連接配接,不讀寫資料,用戶端和服務端分别部署在兩個虛拟機上,當建立50w個連接配接時,服務端消耗2g記憶體,大概每個socket占用4kb,這個4kb是核心申請的空間,并不增加使用者程序的記憶體。至于這個4kb是由哪個部分占用的,我還麼找到答案,sock、inode、file這些中繼資料結構加起來也就一兩百位元組,由于沒有收發資料是以跟tcp讀寫緩存關系不大,而且系統預設的讀寫緩沖區大小均為80k+。