前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >想后台运行没想到导致磁盘满了

想后台运行没想到导致磁盘满了

作者头像
编程珠玑
发布2020-10-27 10:48:31
1.1K0
发布2020-10-27 10:48:31
举报
文章被收录于专栏:编程珠玑编程珠玑

来源:公众号【编程珠玑】

作者:守望先生

ID:shouwangxiansheng

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

找到导致磁盘满的程序

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

代码语言:javascript
复制
$ 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占用比较高,然后可以通过:

代码语言:javascript
复制
$ cd /home
$ du -h --max-depth=1
1.9G    ./shouwang
16K    ./lost+found
1.9G    .

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

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

代码语言:javascript
复制
$ find . -type f -size +800M

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

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

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

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

代码语言:javascript
复制
$ lsof config.json
COMMAND  PID     USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
less     shouwang    r   REG   ,        config.json

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

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

找不到大文件?

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

代码语言:javascript
复制
lsof |grep deleted

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

代码语言:javascript
复制
$ touch test.txt
$ less test.txt

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

代码语言:javascript
复制
$ rm test.txt
$ lsof |grep test.txt |grep deleted
less                    shouwang    r      REG               ,            /home/shouwang/workspaces/shell/testdeleted/test.txt (deleted)

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

代码语言:javascript
复制
$ ls -al /proc//fd
dr-x------  shouwang shouwang   月   : .
dr-xr-xr-x  shouwang shouwang   月   : ..
lrwx------  shouwang shouwang  月   :  -> /dev/pts/
lrwx------  shouwang shouwang  月   :  -> /dev/pts/
lrwx------  shouwang shouwang  月   :  -> /dev/pts/
lr-x------  shouwang shouwang  月   :  -> /dev/tty
lr-x------  shouwang shouwang  月   :  -> '/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的参考实现如下:

代码语言:javascript
复制
//来源:公众号【编程珠玑】
//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();

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

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

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

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

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

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

代码语言:javascript
复制
$ gcc -o daemon daemon.c  #编译
$ ./daemon   #运行
$ ls -al /proc/`pidof daemon`/fd  #查看打开的文件
dr-x------  shouwang shouwang   月   : .
dr-xr-xr-x  shouwang shouwang   月   : ..
lrwx------  shouwang shouwang  月   :  -> /dev/pts/
lrwx------  shouwang shouwang  月   :  -> /dev/pts/
lrwx------  shouwang shouwang  月   :  -> /dev/pts/

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

代码语言:javascript
复制
$  ls -al /proc/`pidof daemon`/fd 
lrwx------  shouwang shouwang  月   :  -> '/dev/pts/4 (deleted)'
lrwx------  shouwang shouwang  月   :  -> '/dev/pts/4 (deleted)'
lrwx------  shouwang shouwang  月   :  -> '/dev/pts/4 (deleted)'

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

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

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

代码语言:javascript
复制
$ at now <<< “./daemon"

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

代码语言:javascript
复制
$ ls -l /proc/`pidof daemon`/fd
lr-x------  shouwang shouwang  月   :  -> '/var/spool/cron/atjobs/a00001019765fe (deleted)'
lrwx------  shouwang shouwang  月   :  -> '/var/spool/cron/atspool/a00001019765fe (deleted)'
lrwx------  shouwang shouwang  月   :  -> '/var/spool/cron/atspool/a00001019765fe (deleted)'

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

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

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

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

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

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

代码语言:javascript
复制
$ tail -  /proc/`pidof daemon`/fd/
daemonize ok
daemonize ok
daemonize ok
daemonize ok
daemonize ok

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

小结

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

代码语言:javascript
复制
/* 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函数

代码语言:javascript
复制
#include <unistd.h>
int daemon(int nochdir, int noclose);

总结

本文主要涉及以下内容:

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-10-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程珠玑 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 找到导致磁盘满的程序
  • 找不到大文件?
  • 不完善的daemon实现
  • 为什么会有这些奇怪的文件?
  • 小结
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档