由unix socket想到的部分文件系统问题
一、unix socket
这种套接口感觉在文件系统和套接口中都是一种异类,就好像蝙蝠是兽中的鸟、鸟中的兽一样。它的特点在于一个地址是否被占用是通过一个文件是否存在来确定。这其实是一个比较危险的操作,因为这个文件的存在是持久性创建的文件,会给底层的文件系统造成持久的影响。现在假设说一个程序非正常结束,例如遇到段错误、被管理员通过SIGKILL杀死等原因而异常退出,那么在下次启动的时候,这个文件还是存在的,这就意味着当前即使没有任何进程在使用(侦听)这个套接口,依然没有任何程序可以使用这个地址。
通常程序在使用相同的unix套接口的时候,都会在bind该套接口之前对套接口执行unlink来删除这个文件,这一点在syslog-ng和fastcgi的实现模块中都是这么做的。
迄今为止,还是没有什么问题。但是对于syslog-ng来说,按照linux下syslog函数的实现规则,它们都是打印到/dev/log套接口文件中,然后系统的日志管理工具来侦听这个套接口。同样滴、在执行bind功能之前,日志工具会删除这个文件,然后自己bind的时候在创建这个文件。迄今为止、还是没有问题。同一个系统可能使用多个不同系统日志收集工具,例如syslogd、rsyslogd、syslog-ng,按照syslog的协议,它们都会尝试在/dev/log文件上进行侦听,为了避免绑定失败,它们都会在bind前删除文件。根据linux文件系统的惯例,如果一个文件正在被使用,那么删除是可以的,但是它只是从内存中删除,而不会通过文件系统立即删除。所以每个日志工具启动的时候,之前的日志管理工具同样还是使用之前自己绑定的/dev/log文件,而新的日志工具在unlink之后通过bind再次创建一个新的/dev/log设备文件。
这看起来也没有什么问题。
在C库的syslog实现中,它会在openlog的时候打开这个侦听的套接口,之后每次执行syslog的时候都是使用这个已经打开的文件进行发送操作,只要客户端不重启,它就一直使用第一次打开时使用的文件描述符。同样只要之前的系统日志工具没有重启,它们之间的连接就会一直有效,而此后新启动的任务将会使用新的侦听进程。这就会出现日志记录不一致的问题。
为了说明这个问题,看一下我的系统的一个输出:
[root@Harry bash-4.1]# ps aux | grep syslog
root 5090 0.0 0.0 4220 692 pts/1 S+ 22:48 0:00 grep syslog
root 22276 0.0 0.1 45688 1244 ? Sl 18:16 0:01 rsyslogd
root 23186 0.0 0.0 3564 712 ? S 18:21 0:00 supervising syslog-ng
root 23187 0.0 0.2 7452 2668 ? Ss 18:21 0:00 /home/tsecer/Downloads/syslog-ng-3.2.3/syslog-ng/.libs/lt-syslog-ng
root 23303 0.0 0.0 3564 704 ? S 18:22 0:00 supervising syslog-ng
root 23304 0.0 0.2 7452 2660 ? Ss 18:22 0:00 /home/tsecer/Downloads/syslog-ng-3.2.3/syslog-ng/.libs/lt-syslog-ng
root 27058 0.0 0.0 3564 712 ? S 18:44 0:00 supervising syslog-ng
root 27059 0.0 0.2 7452 2668 ? Ss 18:44 0:00 /home/tsecer/Downloads/syslog-ng-3.2.3/syslog-ng/.libs/lt-syslog-ng
root 27199 0.0 1.5 28580 16008 pts/5 S+ 18:44 0:00 gdb /home/tsecer/Downloads/syslog-ng-3.2.3/syslog-ng/.libs/lt-syslog-ng
root 27233 0.0 0.0 0 0 ? Z 18:45 0:00 [lt-syslog-ng] <defunct>
root 27265 0.0 0.0 3564 576 ? S 18:45 0:00 supervising syslog-ng
root 27266 0.0 0.2 7452 2644 ? Ss 18:45 0:00 /home/tsecer/Downloads/syslog-ng-3.2.3/syslog-ng/.libs/lt-syslog-ng
[root@Harry bash-4.1]# netstat -anp | grep /dev/log
unix 2 [ ] DGRAM 830241 27266/lt-syslog-ng /dev/log
unix 3 [ ] DGRAM 917054 22276/rsyslogd /dev/log
unix 2 [ ] DGRAM 829315 27059/lt-syslog-ng /dev/log
unix 2 [ ] DGRAM 815520 23304/lt-syslog-ng /dev/log
unix 2 [ ] DGRAM 815017 23187/lt-syslog-ng /dev/log
[root@Harry bash-4.1]#
这里可以看到,对于同一个/dev/log文件,其中对应了六个不同的inode编号,也就是说系统的内存中当前存在6个不同的进程在侦听不同的系统日志输入。
二、文件延迟删除可能导致的一致性问题
linux中正在使用的文件可以被删除,解决可能一些流氓程序长运行而拒绝被杀死的问题,但是这样的操作可能带来文件系统的不一致。和内存泄露一样,可能会存在inode的泄露。在文件系统中,目录中的目录项dentry就相当于指针,而inode相当于指向的数据结构头,文件内容则相当于数据体。在用文件被删除的时候,相当于该文件在文件系统中的dentry会被优先删除,删除之后该文件就从文件系统中消失,从而可以在文件系统中相同文件夹创建新的同名文件。而该文件使用的inode在磁盘上依然处于占用状态,以保证文件的真正内容在磁盘上一直存在到文件使用周期结束。
假设在这个时候文件系统发生断电,此时文件系统就可能出现不一致的情况。因为文件已经被从文件系统中删除,inode没有,相当于出现了文件泄露,inode本身在磁盘上占据的空间并不多,但是文件本身可能占用了大量的磁盘空间,再加上如果这样的文件很多,那么文件系统就相当于浪费了大量的存储空间。
为了验证这个问题,我在一个minix文件系统中进行了测试。为了使用这么不常见的文件系统呢?当然不是怀旧,更不是为了装那啥,而是在busybox中自带的mkfs中没有ext2的格式化工具,只有dos和minix的,由于ext文件系统是基于minix基础上开发的,所以两者本质上是一样的。
![由unix socket想到的部分文件系统问题 - Tsecer - Tsecer的回音岛](http://img0.ph.126.net/WEklWERxA6WImWK_PdgvXQ==/6597467789865587390.jpg)
之后我断电(不是重启)了系统,然后重启,再次挂载该文件系统的时候,minix系统会有下面提示:
MINIX-fs: mounting unchecked file system,running fsck is recommended
也就是说文件系统并没有默默的接收这一错误,而是在挂载文件的时候给出了善意的提示。看了一下ext2文件系统的实现,同样的操作步骤也会造成ext的这样提示。
三、文件系统如何做到这种检测
搜索一下内核中关于这个提示的位置,其位置位于
static int minix_fill_super(struct super_block *s, void *data, int silent)
if (!(s->s_flags & MS_RDONLY)) {
if (sbi->s_version != MINIX_V3) /* s_state is now out from V3 sb */
ms->s_state &= ~MINIX_VALID_FS;如果不是以只读形式挂载的文件系统,则在文件系统挂载的时候将硬盘超级块的状态在内存中复制一份,然后无条件清除硬盘中超级块的状态为不一致状态。
mark_buffer_dirty(bh);
}
if (!(sbi->s_mount_state & MINIX_VALID_FS))如果硬盘中上次保存的状态为不一致状态,给出提示。
printk("MINIX-fs: mounting unchecked file system, "
"running fsck is recommended\n");
else if (sbi->s_mount_state & MINIX_ERROR_FS)
printk("MINIX-fs: mounting file system with errors, "
"running fsck is recommended\n");
那么硬盘中的这种状态是在什么时候清除的呢?对应的(不是明显地)清除是在超级块的删除,也就是文件系统的卸载中修改为一致状态。
static void minix_put_super(struct super_block *sb)
int i;
struct minix_sb_info *sbi = minix_sb(sb);
if (!(sb->s_flags & MS_RDONLY)) {
if (sbi->s_version != MINIX_V3) /* s_state is now out from V3 sb */
sbi->s_ms->s_state = sbi->s_mount_state;
mark_buffer_dirty(sbi->s_sbh);
}
在卸载文件系统的时候,挂载前文件系统中保存的状态将会被重新写回到硬盘中。由于新的硬盘在格式化之后是一致状态,如果之后每次挂载和卸载一一对应,那么任意多次挂在和卸载之后,文件系统的状态应该和起始状态一致,那就是出于合法状态。但是如果某此挂载之后没有正常卸载,那么之后硬盘中就会记录这种不一致状态。
四、df如何实现
通过strace可以看到,df是通过fstatfs工具来获得文件系统的特征的,包括inode的总量和在用量。这里需要提醒的是,这个df是disk filesystem的意思,而不是disk format,所以大家不用担心这个工具会格式化分区。
minix文件系统的底层实现比较简单
static int minix_statfs(struct dentry *dentry, struct kstatfs *buf)
{
struct minix_sb_info *sbi = minix_sb(dentry->d_sb);
buf->f_type = dentry->d_sb->s_magic;
buf->f_bsize = dentry->d_sb->s_blocksize;
buf->f_blocks = (sbi->s_nzones - sbi->s_firstdatazone) << sbi->s_log_zone_size;
buf->f_bfree = minix_count_free_blocks(sbi);
buf->f_bavail = buf->f_bfree;
buf->f_files = sbi->s_ninodes;
buf->f_ffree = minix_count_free_inodes(sbi);
buf->f_namelen = sbi->s_namelen;
return 0;
}
五、简单流程整理
1、文件删除是目录项删除
ext2_unlink--->>ext2_delete_entry
if (pde)
from = (char*)pde - (char*)page_address(page);
lock_page(page);
err = mapping->a_ops->prepare_write(NULL, page, from, to);
BUG_ON(err);
if (pde)
pde->rec_len = cpu_to_le16(to-from);dentry内容被合并入前一dentry中。
dir->inode = 0;应用的inode清零。
此时在iput的时候文件应用不为零,所以文件不能删除。
2、文件删除
当使用该文件的进程退出之后,关闭文件描述符,内存inode引用为零,并且nlink为零,所以需要从文件系统中删除该文件,大致流程为
minix_delete_inode--->>minix_free_inode
bh = sbi->s_imap[ino];
lock_kernel();
if (!minix_test_and_clear_bit(bit, bh->b_data))
printk("minix_free_inode: bit %lu already cleared\n", bit);
其中sbi->imap就是statfs中minix_count_free_inodes中扫描的数据结构位图。