之前在《如何让程序真正地后台运行》一文中提到了程序后台运行的写法,但是里面的示例程序在某些场景下是会有问题的,这里先不说什么问题,我们先看看这个磁盘满的问题是怎么产生的,通过这篇文章你将会学习到大量linux命令的实操使用。
找到导致磁盘满的程序
当发现磁盘占用比较多的时候,可以通过下面的命令,查看各个挂载路径的占用情况:
1 | df -h |
当然我这里并没有哪个挂载路径的磁盘占用率比较高,这里假设home占用比较高,然后可以通过:
1 | $ cd /home |
这样可以逐层知道哪些目录有了不该有的大文件。
当然你也可以使用find直接找出大文件,比如查找当前目录下大于800M的文件:1
$ find . -type f -size +800M
find的用法可以参考《find命令高级用法》。
如果找到了该文件,并且确认是无用文件,那么就可以删除了。
但是如果仍然有程序打开了该文件,那么即便你删除了文件,其占用的磁盘空间也并不会释放,因为仍然它的”文件引用”不是0,文件并不会被删除。
在《rm删除文件空间就释放了吗?》一文中,有更加详细的解释。
所以你需要看一下,是否还有程序打开该文件,举个例子:1
2
3$ 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命令看到某些挂载路径磁盘占用率比较高,但是始终找不到大文件,那么你就要考虑,是不是大文件看似被删除了,但是还有程序打开。要找到这样的文件,其实也很简单,前面已经介绍过了:1
lsof |grep deleted
lsof能看到被打开的文件,而如果文件被删除了(比如使用rm命令),但是仍然有程序打开,则会是deleted状态,举个例子:1
2$ touch test.txt
$ less test.txt
创建一个文件test.txt,并随意输入一些内容,然后使用less命令打,随后在另一个终端,删除该文件:1
2
3$ 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,我们看一下这个程序打开的文件:1
2
3
4
5
6
7
8
9$ 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的参考实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72//来源:公众号【编程珠玑】
//https://www.yanbinghu.com
/*实现仅供参考,可根据实际情况调整*/
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以上的文件描述符。
在其中一个终端运行上面的例子:1
2
3
4
5
6
7
8$ 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打开的是程序所在的终端,这时关闭该终端,在另外一个终端执行:1
2
3
4$ 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命令启动:1
$ at now <<< “./daemon"
at命令表示当前时间执行daemon程序。
再看看它打开的文件:1
2
3
4$ 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状态,最终可能会导致磁盘空间占用不断增大,但是又找不到实际的大文件。
为了验证我们的想法,可以看下前面的文件内容到底是什么:1
2
3
4
5
6$ tail -5 /proc/`pidof daemon`/fd/1
daemonize ok
daemonize ok
daemonize ok
daemonize ok
daemonize ok
看到了吗,这既是我们程序的打印!竟然打印到一个毫无相关的文件中了。
小结
从上面的例子可以看到,要想实现一个线上可用的daemon程序,还必须重定向标准输入,标准输出和标准错误,比例:1
2
3
4/* 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函数:1
2
int daemon(int nochdir, int noclose);
总结
本文主要涉及以下内容:
- 查看各挂载路径空间占用情况
- 查看目录空间占用情况
- 如何创建子进程—《如何创建子进程?》
- 标准输入,输出和标准错误—《如何理解Linux shell中”2>&1”》
- 查看进程打开文件信息—《如何查看linux中文件打开情况》
- 查找大文件—《find命令高级用法》
- /dev/null特殊文件的用法 —《linux下这些特殊的文件》
- 查找被删除但仍有进程占用的文件
- 编写daemon程序注意事项