后台程序实现不当导致磁盘满的问题

 

之前在《如何让程序真正地后台运行》一文中提到了程序后台运行的写法,但是里面的示例程序在某些场景下是会有问题的,这里先不说什么问题,我们先看看这个磁盘满的问题是怎么产生的,通过这篇文章你将会学习到大量linux命令的实操使用。

找到导致磁盘满的程序

当发现磁盘占用比较多的时候,可以通过下面的命令,查看各个挂载路径的占用情况:

$ df -h
udev            3.9G     0  3.9G    0% /dev
tmpfs           784M  2.0M  782M    1% /run
/dev/sda11       19G  6.5G   12G   37% /
tmpfs           3.9G   91M  3.8G    3% /dev/shm
tmpfs           5.0M  4.0K  5.0M    1% /run/lock
tmpfs           3.9G     0  3.9G    0% /sys/fs/cgroup
/dev/sda12      9.4G   37M  8.8G    1% /tmp
/dev/sda14      6.4G  168M  5.9G    3% /boot
/dev/sda10       57G  2.0G   52G    4% /home
/dev/sda1       256M   33M  224M   13% /boot/efi
tmpfs           784M   16K  784M    1% /run/user/121
tmpfs           784M   44K  784M    1% /run/user/1000

当然我这里并没有哪个挂载路径的磁盘占用率比较高,这里假设home占用比较高,然后可以通过:

$ cd /home
$ du -h --max-depth=1
1.9G    ./shouwang
16K    ./lost+found
1.9G    .

这样可以逐层知道哪些目录有了不该有的大文件。

当然你也可以使用find直接找出大文件,比如查找当前目录下大于800M的文件:

$ find . -type f -size +800M

find的用法可以参考《find命令高级用法》。

如果找到了该文件,并且确认是无用文件,那么就可以删除了。

但是如果仍然有程序打开了该文件,那么即便你删除了文件,其占用的磁盘空间也并不会释放,因为仍然它的"文件引用"不是0,文件并不会被删除。
在《rm删除文件空间就释放了吗?》一文中,有更加详细的解释。

所以你需要看一下,是否还有程序打开该文件,举个例子:

$ lsof config.json
COMMAND  PID     USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
less    6750 shouwang    4r   REG   8,10      233 3411160 config.json

从上面的结果,可以看到,是less程序打开了config.json文件,并且它的进程id是6750。

找到进程之后,根据实际情况决定是否需要停止程序,然后删除大文件。

找不到大文件?

现实常常可能不如意,比如虽然可以通过df命令看到某些挂载路径磁盘占用率比较高,但是始终找不到大文件,那么你就要考虑,是不是大文件看似被删除了,但是还有程序打开。要找到这样的文件,其实也很简单,前面已经介绍过了:

lsof |grep deleted

lsof能看到被打开的文件,而如果文件被删除了(比如使用rm命令),但是仍然有程序打开,则会是deleted状态,举个例子:

$ touch test.txt
$ less test.txt

创建一个文件test.txt,并随意输入一些内容,然后使用less命令打,随后在另一个终端,删除该文件:

$ rm test.txt
$ lsof |grep test.txt |grep deleted
less      6989              shouwang    4r      REG               8,10       134    3541262 /home/shouwang/workspaces/shell/testdeleted/test.txt (deleted)

可以看到打开该文件的进程id为6989,我们看一下这个程序打开的文件:

$ ls -al /proc/6989/fd
dr-x------ 2 shouwang shouwang  0 10月  6 10:57 .
dr-xr-xr-x 9 shouwang shouwang  0 10月  6 10:56 ..
lrwx------ 1 shouwang shouwang 64 10月  6 10:57 0 -> /dev/pts/1
lrwx------ 1 shouwang shouwang 64 10月  6 10:57 1 -> /dev/pts/1
lrwx------ 1 shouwang shouwang 64 10月  6 10:57 2 -> /dev/pts/1
lr-x------ 1 shouwang shouwang 64 10月  6 10:57 3 -> /dev/tty
lr-x------ 1 shouwang shouwang 64 10月  6 10:57 4 -> '/home/shouwang/workspaces/shell/testdeleted/test.txt (deleted)'
$ du -h 

(关于proc虚拟文件系统,可以参考《Linux中不可错过的信息宝库》)。从上面也可以看到,文件描述符4的文件为test.txt,但是deleted状态。

停止这个进程,你会发现所占用的磁盘空间会被释放。

不完善的daemon实现

通常在终端启动一个程序后,文件描述符0,1,2通常对应标准输入,标准输出,标准错误。从前面的例子中也能窥见一二,它打开的是/dev/pts/1,其实就是当前终端。更多信息可以参考《如何理解Linux shell中“2>&1”》。

回到开始的问题,之前例子中daemonize的参考实现如下:

//来源:公众号【编程珠玑】
//https://www.yanbinghu.com
#include<stdio.h>
#include<sys/resource.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
/*实现仅供参考,可根据实际情况调整*/
int daemonize()
{
    /*清除文件权限掩码*/
    umask(0);

    /*父进程退出*/
    pid_t pid;
    if((pid=fork()) < 0)
    {
        /*for 出错*/
        perror("fork error");
        return -1;
    }
    else if(0 != pid)/*父进程*/
    {
        printf("father exit\n");
        exit(0);
    }
    /*子进程,成为组长进程,并且摆脱终端*/
    setsid();

    /*修改工作目录*/
    if(chdir("/") < 0)
    {
        perror("change dir failed");
        return -1;
    }

    struct rlimit rl;
    /*先获取文件描述符最大值*/
    if(getrlimit(RLIMIT_NOFILE,&rl) < 0)
    {
        perror("get file decription failed");
        return -1;
    }
    /*如果无限制,则设置为1024*/
    if(rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;

    /*为了使得终端有输出,保留了文件描述符0,1,2;实际上父进程可能没有打开2以上的文件描述符*/
    int i;
    for(i = 3;i < rl.rlim_max;i++)
        close(i);
    return 0;
}
int main(void)
{
    if(0 == daemonize())
    {
        while(1)
        {
            printf("daemonize ok\n");
            sleep(2);
        }
    }
    else
    {
        printf("daemonize failed\n");
        sleep(1);
    }
    return 0;
}

这里注意到,daemonize函数最后关闭了2以上的文件描述符。

在其中一个终端运行上面的例子:

$ gcc -o daemon daemon.c  #编译
$ ./daemon   #运行
$ ls -al /proc/`pidof daemon`/fd  #查看打开的文件
dr-x------ 2 shouwang shouwang  0 10月  6 11:26 .
dr-xr-xr-x 9 shouwang shouwang  0 10月  6 11:26 ..
lrwx------ 1 shouwang shouwang 64 10月  6 11:26 0 -> /dev/pts/4
lrwx------ 1 shouwang shouwang 64 10月  6 11:26 1 -> /dev/pts/4
lrwx------ 1 shouwang shouwang 64 10月  6 11:26 2 -> /dev/pts/4

可以看到0,1,2打开的是程序所在的终端,这时关闭该终端,在另外一个终端执行:

$  ls -al /proc/`pidof daemon`/fd 
lrwx------ 1 shouwang shouwang 64 10月  6 11:26 0 -> '/dev/pts/4 (deleted)'
lrwx------ 1 shouwang shouwang 64 10月  6 11:26 1 -> '/dev/pts/4 (deleted)'
lrwx------ 1 shouwang shouwang 64 10月  6 11:26 2 -> '/dev/pts/4 (deleted)'

发现0,1,2都是deleted状态了,因为关闭前面启动程序的终端后,也相当于删除了它标准输入输出和标准错误指向的文件。

实际上,到这里,都没有任何问题,程序中的printf打印最多无法打印出来而已。

但是,如果程序不是终端启动的呢?或者说没有终端的环境,比如crontab启动,at命令启动:

$ at now <<< “./daemon"

at命令表示当前时间执行daemon程序。
再看看它打开的文件:

$ ls -l /proc/`pidof daemon`/fd
lr-x------ 1 shouwang shouwang 64 10月  6 11:42 0 -> '/var/spool/cron/atjobs/a00001019765fe (deleted)'
lrwx------ 1 shouwang shouwang 64 10月  6 11:42 1 -> '/var/spool/cron/atspool/a00001019765fe (deleted)'
lrwx------ 1 shouwang shouwang 64 10月  6 11:42 2 -> '/var/spool/cron/atspool/a00001019765fe (deleted)'

看见没有,你会发现它打开了一些奇怪的文件。

为什么会有这些奇怪的文件?

很明显,我们自己写的程序中并没有打开这样的文件,但是从文件名可以推断,它看能是cron程序打开的。那么怎么会变成daemon程序打开了呢?

这要从fork说起,之前在《如何创建子进程?》中说到过,fork出来的子进程会继承父进程的文件描述符,我们的daemon实现已经将2以上的描述符关闭了,但是并没有关闭0,1,2,而由于daemon程序自己实际上没有打开任何文件,0,1,2是空着的,实际上就变成了打开的是父进程曾经打开的文件。

但是由于printf持续向标准输出打印信息,即不断向描述符1打开的文件写入内容,而该文件又是deleted状态,最终可能会导致磁盘空间占用不断增大,但是又找不到实际的大文件。

为了验证我们的想法,可以看下前面的文件内容到底是什么:

$ tail -5  /proc/`pidof daemon`/fd/1
daemonize ok
daemonize ok
daemonize ok
daemonize ok
daemonize ok

看到了吗,这既是我们程序的打印!竟然打印到一个毫无相关的文件中了

小结

从上面的例子可以看到,要想实现一个线上可用的daemon程序,还必须重定向标准输入,标准输出和标准错误,比例:

/* redirect stdin, stdout, and stderr to /dev/null */
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);

如果我们不关心这些输入输出,则重定向到/dev/null,相当于丢弃该内容,关于/dev/null,这里有更多的介绍《linux下这些特殊的文件》。

是否要重定向标准输入输出,完全取决于你的实际应用场景,比如某些情况你可能就是需要将标准输出指向父进程的文件,则可以不需要重定向。当然了,至于实现,更推荐的做法是调用daemon函数

#include <unistd.h>
int daemon(int nochdir, int noclose);

总结

本文主要涉及以下内容:

 

关注公众号【编程珠玑】,获取更多Linux/C/C++/数据结构与算法/计算机基础/工具等原创技术文章。后台免费获取经典电子书和视频资源

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页