刨根问底儿,看我如何处理 Too many open files 错误!

点击上方蓝字"开发内功修炼",关注并设为星标

飞哥的硬核文章将第一时间送达~~~

如果你的项目中支持高并发,或者是测试过比较多的并发连接。那么相信你一定遇到过“Too many open files”这个错误。

这个错误的出现其实是正常的,因为每打开一个文件(包括socket),都需要消耗一定的内存资源。为了避免个别进程不受控制地打开了过多的文件而让整个服务器崩溃,Linux 对打开的文件描述符数量有限制。

但是解决这个错误“奇葩”的地方在于,竟然需要修改三个参数:fs.nr_open、nofile(其实 nofile 还分 soft 和 hard) 和 fs.file-max。这几个参数里有的是进程级的、有的是系统级的、有的是用户进程级的,说一遍都觉得好乱。而且另外这几个参数还有依赖关系,着实比较复杂。

不知道你,反正飞哥我是根本记不住哪个是哪个。每次遇到这种问题,还是都得再继续 Google 一遍。但由于复杂性,所以其实网上的很多帖子里也都并没有真正搞清楚。如果照搜索出来的文章改,稍有不慎就会踩雷,把机器搞出问题。

我在测试最大TCP连接数的时候就踩过两次坑。

第一次是当时开了二十个子进程,每个子进程开启了五万个并发连接兴高采烈准备测试百万并发。结果倒霉催地忘了改 file-max 了。实验刚开始没多大一会儿就开始报错“Too many open files”。但问题是这个时候更悲催的是发现所有的命令包括 ps、kill也同时无法使用了。因为它们也都需要打开文件才能工作。后来没办法,重启系统解决的。

另外一次是重启机器完了之后发现无法 ssh 登录了。后来找运维工程部的同学报障以后才算是修复。最终发现是因为 hard nofile 比 fs.nr_open 高了,直接导致无法登陆。(其实我把 fs.nr_open 加大过,但是用的是 echo 命令 修改的。系统一重启,还原了)。

一、找到源代码

对于这三个家伙,我真的是无法言语更多了。所以我下定了决心,一定要把它们彻底搞清楚。怎么搞?那没有比把它的源码扒出来能看的更准确了。我们就拿创建 socket 来举例,首先找到 socket 系统调用的入口

//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 调用 sock_map_fd 来创建相关内核对象。接着我们再进入 sock_map_fd 瞧瞧。

//file: net/socket.c
static int sock_map_fd(struct socket *sock, int flags)
{
 struct file *newfile;

 //在这里会判断打开文件数是否超过 soft nofile 和 fs.nr_open
 //获取 fd 句柄号
 int fd = get_unused_fd_flags(flags);  
 if (unlikely(fd < 0))
  return fd;

 //在这里会判断打开文件数是否超过 fs.file-max
 //创建 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);
}

为什么创建一个socket又要申请 fd,又要申请 sock_alloc_file 呢?我们看一个进程打开文件时的内核数据结构图就明白了

结合上图,就能轻松理解这两个函数的作用

  • get_unused_fd_flags:申请 fd,这只是一个在找一个可用的数组下标而已

  • sock_alloc_file:申请真正的 file 内核对象

二、找到进程级限制 nofile 和 fs.nr_open

接下来,我们再回到最大文件数量的判断上。这里我直接把结论抛出来。get_unused_fd_flags 中判断了 nofile、和 fs.nr_open。如果超过了这两个参数,就会报错。请看!

//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 中,调用了 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 开始的整数。如果超限,就报错 EMFILE (Too many open files)。

这里注意个小细节,那就是进程里的 fd 是一个从 0 开始的整数。只要确保分配出去的 fd 编号不超过 limits.conf 中 nofile,就能保证该进程打开的文件总数不会超过这个数。

接着我们看到调用又会进入 expand_files:

static int expand_files(struct files_struct *files, int nr)
{
 //2. 判断打开文件数是否超过 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 比较,都用的是当前进程的文件描述符序号在比较的,所以这两个参数都是进程级别的。

有意思的是和这两个参数的比较几乎是前后脚进行的,所以它两的作用也基本一样。Linux之所以分两个参数来控制,那是因为 fs.nr_open 是系统全局的,而 nofile 则可以分用户来分别控制。

所以,现在我们可以得出第一个结论。

结论1:soft nofile 和 fs.nr_open的作用一样,它两都是限制的单个进程的最大文件数量。区别是 soft nofile 可以按用户来配置,而 fs.nr_open 所有用户只能配一个

三、找到系统级限制 fs.nr_open

我们在回过头来看 sock_map_fd 中调用的另外一个函数 sock_alloc_file,在这个函数里我们发现它会和 fs.file-max 这个系统参数来比较。用啥比的呢?

//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参数
 if (get_nr_files() >= files_stat.max_files 
  && !capable(CAP_SYS_ADMIN) //注意这里root账号并不受限制
  ) {
 }
}

可见是用 get_nr_files() 来和 fs.file-max来比较的。根据该函数的注释我们能看到它是当前系统打开的文件描述符总量。如下:

/*
 * Return the total number of open files in the system
 */
static long get_nr_files(void)
{
 ...

另外注意下 !capable(CAP_SYS_ADMIN) 这行。看完这句,我才恍然大悟,原来 file-max 这个参数只限制非 root 用户。开篇中我提到的文件打开过多时无法使用 ps,kill 等命令,是因为我用的非 root 账号操作的。哎,下次再遇到这种文件直接用 root 去 kill 就行了。之前竟然丢脸地采用了重启机器大法。。

所以现在我们可以得出另一个结论了。
结论2:fs.file-max: 整个系统上可打开的最大文件数,但不限制 root 用户

总结一下

我们总结一下,其实在 Linux 上能打开多少个文件,限制有两种:

  • 第一种,进程级别的,限制的是单个进程上可打开的文件数。具体参数是 soft nofile 和 fs.nr_open。它们两个的区别是 soft nofile 可以不同用户配置不同的值。而 fs.nr_open 在一台 Linux 上只能配一次。

  • 第二种,系统级别的,整个系统上可打开的最大文件数,具体参数是fs.file-max。但是这个参数不限制 root 用户。

另外这几个参数之间还有耦合关系,因此还要注意以下三点:

  • 1、如果你想加大 soft nofile,  那么 hard nofile 也需要一起调整。因为如果 hard nofile 设置的低, 你的 soft nofile 设置的再高都没用,实际生效的值会按二者里最低的来。

  • 2、如果你加大了 hard nofile,那么 fs.nr_open 也都需要跟着一起调整。如果不小心把 hard nofile 设置的比 fs.nr_open 大了,后果比较严重。会导致该用户无法登陆。如果设置的是 * 的话,那么所有的用户都无法登陆。

  • 3、还要注意如果你加大了 fs.nr_open,但是用的是 echo "xx" > ../fs/nr_open 的方式,刚改完你可能觉得没问题。只要机器一重启你的 fs.nr_open 设置就会失效,还是会无法登陆。

假如你想让你的进程可以打开 100 万个文件描述符,我觉得比较稳妥点的修改方法是干脆都直接用 conf 文件的方式来改。这样比较统一,也比较安全。

# 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

通过这种方式修改,你就可以绕过飞哥踩过的坑了。

另外飞哥建立了一个技术群,欢迎大家加入,一起成长。先加我微信(zhangyanfei748527), 我来拉入群!

内功修炼 Github 也开通了,公众号不让嵌入外链,所以只能辛苦各位复制如下网址了
https://github.com/yanfeizhang/coder-kung-fu


<think>好的,我现在需要帮助用户彻底理解一个简单的“Hello,World!”程序的每个细节。用户提到他们知道某些代码结构,比如#include<iostream>的作用,但想深入了解为什么需要这些以及每个部分的具体原因。首先,我需要分解一个典型的C++ Hello World程序,逐行解释每个部分的功能和背后的原理。 首先,用户提供的示例代码是: #include<iostream> usingnamespace std; intmain() {cout << "Hello, World!" << endl; return0;} 我需要逐一解释每个语句。首先处理#include <iostream>,这部分是预处理指令,用来包含标准输入输出库,让程序能够使用cout和cin等对象。用户可能不明白为什么需要包含这个头文件,以及如果不包含会怎样。需要说明iostream提供了输入输出功能的基础设施,比如cout的定义。 接下来是using namespace std;。这里需要解释命名空间的概念,以及std命名空间包含了标准库的函数和对象。如果不使用这行,每次调用cout时都需要写std::cout,这样是为了避免命名冲突,比如自己定义的cout和标准库的冲突。 然后是intmain() { ...},主函数作为程序的入口点。需要说明main函数的返回类型int,以及操作系统如何通过返回值判断程序是否正常结束。return 0表示程序成功退出,非零值通常表示错误代码。在函数体内,cout << "Hello, World!" << endl; 这一行需要详细解释。cout是输出流对象,<<是流插入运算符,将字符串发送到标准输出。endl不仅是换行,还刷新输出缓冲区,确保内容立即显示。可能还要提到缓冲区的概念,以及为什么有时候需要立即刷新。 用户可能会问,为什么用<<而不是其他符号?这涉及到运算符重载,C++中允许自定义运算符的行为,这里iostream库重载了<<用于输出。此外,可以解释多个<<连用的原理,每个<<返回流对象,允许链式调用。 然后,return0;的作用,以及main函数返回值的意义。用户可能想知道如果返回其他值会怎样,比如1,操作系统如何处理这些返回值。 用户可能还关心编译和链接的过程,比如预处理阶段如何处理#include指令,将头文件内容插入到源代码中,然后编译器编译成目标代码,链接器将目标代码与标准库链接生成可执行文件。此外,用户可能对程序执行的流程有疑问,比如操作系统如何加载程序,调用main函数,执行完后再将控制权交还给操作系统。 需要确保解释每个细节时,用简单易懂的语言,避免专业术语过多,必要时举例说明。例如,解释命名空间时,可以比喻为姓氏,避免不同家族同名的人混淆。 可能还需要比较C++与其他语言的不同,比如Python的print语句不需要包含头文件或命名空间,因为语言设计不同,解释型语言和编译型语言的差异。 最后,用户可能会提出更深层次的问题,比如头文件的内容,如何查看iostream的具体代码,或者如何处理多个源文件的情况,但根据当前问题,主要集中在Hello World程序的每个部分。 总结需要覆盖的点: 1. #include<iostream>的作用和必要性 2.using namespace std;的意义和命名空间的概念 3.main函数的角色和返回类型 4. cout和<<运算符的工作原理 5. endl的作用和缓冲区刷新 6. return 0的意义 在回答时要逐条详细解释,确保用户理解每个细节。同时,准备相关的问题,比如头文件的其他例子、不同命名空间的使用、main函数的不同写法、其他输出方法,以及预处理的其他指令等。</think>以下是对C++版"Hello, World!"程序的逐行深度解析: ```cpp #include <iostream> // 预处理指令引入输入输出库 using namespace std; // 使用标准命名空间 int main() { // 程序入口函数 cout << "Hello, World!" << endl; // 输出语句 return 0; // 返回值 } ``` **1. `#include <iostream>`** 这是预处理器指令,作用是将标准输入输出库的内容完整复制到当前文件中。 - `iostream`包含:`cout`(标准输出)、`cin`(标准输入)、`cerr`(标准错误)、`clog`(缓冲日志)等对象 - 没有这个包含语句,编译器将不认识`cout`对象,导致编译错误 - 类比:就像使用电器需要插电,使用输出功能需要"接上"输入输出库 **2. `using namespace std;`** 这是命名空间声明: - `std`是C++标准库的命名空间,包含所有标准库组件 - 没有这个声明时,需要写`std::cout`才能使用标准库对象 - 命名空间作用:防止不同库的同名标识符冲突(如自己定义的`cout`和标准库的`cout`) - 类比:就像用"张伟"需要说明是"张家.伟"还是"王家.伟" **3. `int main()`** 程序唯一入口点: - 操作系统执行程序时自动寻找并执行此函数 - `int`表示必须返回整数(0表示成功,非0表示错误代码) - 空括号`()`表示不接受任何参数(也可以写`int main(void)`) **4. `cout << "..." << endl;**` 输出语句的完整解析: - `cout`:标准输出流对象(console output) - `<<`:流插入运算符(左移运算符被重载为输出操作) - 运算符链式调用:`(cout << "Hello")`会返回`cout`对象,继续执行`.operator<<(endl)` - `endl`:换行并立即刷新输出缓冲区(等价于`'\n' + flush`) **5. `return 0;`** 程序退出状态: - 0表示程序正常结束 - UNIX系统中可通过`echo $?`查看上一个程序的返回值 - C++标准规定:main函数省略return时默认返回0(但显式写出更好) **完整执行流程**: 预处理器展开头文件 → 编译器编译代码 → 链接器连接标准库 → 操作系统加载执行 → 调用main函数 → 执行输出 → 返回状态码 → 结束程序
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值