linux文件描述符限制和单机最大长连接数
来源:互联网 发布:sql 查询过期时间 编辑:程序博客网 时间:2024/06/01 20:49
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;}
根据以上代码,文件的打开主要分为两个步骤:
- 先是获取一个空闲可用的文件描述符fd;
- 然后再打开文件,并和上一步获取的fd关联。
获取空闲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; ...}
接下来看怎么扩展fdtable的, 下面才是解开真相的地方:
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_open1048576
1048576正好是1024*1024,跟内核代码里面的默认值一样,现在把这个值改成2147483647:
root@localhost proc]# echo 2147483647 > /proc/sys/fs/nr_openbash: 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_open2147483584
因此nr_open
最大值受int类型限制成立。
再看下nofile的最大值,先改hard nofile:
[root@localhost proc]# ulimit -n 2147483584 -H[root@localhost proc]# ulimit -n 2147483585 -Hbash: ulimit: open files: cannot modify limit: Operation not permitted[root@localhost proc]# ulimit -n -H2147483584
hard nofile的最大值正好等于刚才设置nr_open,符合预期,接着看下soft nofile:
[root@localhost proc]# ulimit -n 2147483584 -S[root@localhost proc]# ulimit -n 2147483585 -Sbash: ulimit: open files: cannot modify limit: Invalid argument[root@localhost proc]# ulimit -n -S2147483584
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-max18446744073709551615[root@localhost proc]# echo 18446744073709551616 > /proc/sys/fs/file-max[root@localhost proc]# cat /proc/sys/fs/file-max0
可以修改成功,再大就回到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-max1000[root@localhost ~]# ulimit -n2147483584[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-max97790[root@localhost xuwei]# cat /proc/sys/fs/nr_open1048576[root@localhost xuwei]# echo 80000 > /proc/sys/fs/nr_open[root@localhost xuwei]# cat /proc/sys/fs/nr_open80000
理论和实践证明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_range1024 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+。
由于虚拟机内存有限,不能进行更多连接的测试。不过从理论上分析,在内存足够的情况下,单机应该能达到20+亿的连接,也就是文件描述符的上限。由此可见单机最大连接数主要受内存限制,至于每个连接占用多少内存,每个系统,每个应用可能都不太一样,有的应用的数据流量比较大,占用的用户缓存和系统缓存也相应的会比较大,还要以实际压测的结果为准。
(在压测过程中出现一个问题:当连接达到一定数目时,连接的速度开始变得非常慢,查看系统message日志,会看到“kernel nf_conntrack:table full, dropping packet
”输出,这时候需要调整一下这个参数/proc/sys/net/netfilter/nf_conn_track_count
,这是系统用来记录tcp连接的哈希表的大小)。
- linux文件描述符限制和单机最大长连接数
- 提高linux对最大进程数和最大打开文件描述符数的限制
- 查看、修改linux系统的最大链接数限制、文件描述符限制、端口范围限制、虚拟内存等
- 查看、修改linux系统的最大链接数限制、文件描述符限制、端口范围限制、虚拟内存等
- Linux最大打开文件描述符数
- Linux最大打开文件描述符数
- Linux最大打开文件描述符数
- Linux最大打开文件描述符数
- Linux最大打开文件描述符数
- Linux最大打开文件描述符数
- Linux最大打开文件描述符数
- Linux最大打开文件描述符数
- Linux最大打开文件描述符数
- Linux最大文件句柄(文件描述符)限制和修改
- 修改Linux系统下的最大文件描述符限制
- 修改Linux系统下的最大文件描述符限制
- linux下最大文件描述符的限制
- 更改linux的最大文件描述符限制
- UrJTAG 使用和扩展
- Test5
- java线程同步机制以及对象锁机制
- 使用RecyclerView实现ListView,GridView的效果(上下,左右滑动),拖拽与滑动删除
- 思维导图的三招十八式之梳理
- linux文件描述符限制和单机最大长连接数
- Android 不锁屏
- NoSuchMethodError
- BOOT启动阶段的初始化
- Cygwin编译可执行文件独立运行
- 基于react-native-swiper 封装的bannerView
- css写提示框 ‘正在加载请。。。。。’
- 旧笔记本改造数码相框
- 详细的Webpack配置教程