这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战
之前曾经遇到一个问题,当FTP服务器磁盘没有空间时,设备会不断复位,拿到log后查看,发现一个通信所用的文件打开失败。不断打印Too many open file,然后超时设备复位。同时我们看到数据库文件打开失败,无法写入数据。经过分析、调试、自测,最终解决了问题。根本原因是FTP模块用到的socket句柄未关闭引起的。
linux系统的文件句柄是有限制的。查看系统支持句柄最大值用如下命令:
# ulimit -n
1024
可见,默认是1024。 下面用代码来说明一下问题。代码如下:
/**
系统一次允许最大的文件句柄为1024。
open foobar file for: 340
open bar file for: 340
open foo file for: 341
open foobar file failed: : Too many open files
open BAR file failed: : Too many open files
# ulimit -a
...
open files (-n) 1024
...
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int foo(void)
{
static int cnt = 1;
int ret = 0;
int fd =-1;
char buf[5] = {0};
char read_buf[5] = {0};
fd = open("/tmp/FOO.txt",O_RDWR|O_CREAT,0666);
if(fd < 0)
{
perror("open FOO file failed: ");
return -1;
}
printf("open foo file for: %d\n", cnt++);
return 0;
}
int bar(void)
{
static int cnt = 1;
int ret = 0;
int fd =-1;
char buf[5] = {0};
char read_buf[5] = {0};
fd = open("/tmp/BAR.txt",O_RDWR|O_CREAT,0666);
if(fd < 0)
{
perror("open BAR file failed: ");
return -1;
}
printf("open bar file for: %d\n", cnt++);
return 0;
}
int foobar(void)
{
static int cnt = 1;
int ret = 0;
int fd =-1;
char buf[5] = {0};
char read_buf[5] = {0};
fd = open("/tmp/foobar.txt",O_RDWR|O_CREAT,0666);
if(fd < 0)
{
perror("open foobar file failed: ");
return -1;
}
printf("open foobar file for: %d\n", cnt++);
return 0;
}
int main(void)
{
int ret = 0;
while (1) {
foo();usleep(1);
foobar();usleep(1);
ret = bar();
// if (ret < 0) break; // 不让其退出
usleep(100);
}
return 0;
}
要查看这个进程占用文件句柄数,先获取该进程PID:
# ps -ef | grep a.out
root 17835 7615 17 23:01 pts/1 00:00:00 ./a.out
我们查看该进程文件句柄最大值:
# cat /proc/17835/limits | grep "files"
Max open files 1024 4096 files
和系统默认值一样。查看已占用值:
# ll /proc/17835/fd | wc -l
1027
我们看到,已经占用上千个句柄,一直未释放。当达到系统最大值时,就会报Too many open files的错误。
经分析源码,FTP模块的客户端实现,是需要打开2个socket的,一个是命令通道,一个是数据通道。在服务器没有磁盘空间情况下,write数据是返回错误的,使用FTP模块者认为是无法登陆,下次会再次尝试登陆——在这种情况下,登陆是正常的,只是无法写数据。
但是,每次登陆,都会创建命令通道的socket,这导致了socket的泄漏,不会关闭,因为使用者并没有调用logout函数退出登陆。另一个问题是最主要的,每次写数据时,要创建数据通道的socket,但在出错时,并没有关闭数据通道的socket,而是直接返回。这再次导致了socket泄漏。
找到了原因,解决起来就好办了。检查返回语句,将对应的socket关闭即可。