不知道是不是因为我最近实在是看不到什么构成问题的提问,好多问题连描述都看不懂,所以难得推过来一个新问题,虽然题目我刚看到时有点疑惑,但是结合下面细节描述我承认我确实看懂他问的是什么了(忽然有一种莫名的感动,亦或是悲哀)。既然这是个明确的问题,且描述较清楚,而且赶上我现在正好在等一个运行结果所以有些时间,那么就让我们开始这个问题的回答之旅吧。
遇到这种问题我们首先想到的就是翻阅文档。但是我知道这种问题可能有很多论坛贴吧式的不名来源的表述,我不是说这些表述一定是错的,但是对于本身就不能辨别真伪的初学者来说,从权威文档中寻找解释的意义更大。除了能让你获得更准确更原始的信息表述以外,有时还能让你看到知识本来的样子以及它的演变。
第一个想到的是就是直接"man ls",但是很遗憾,里面只提到ls -l使用了long-format的输出形式,但是并没有给出long-format的具体解释。
于是我想到ls命令并不是Linux首创的,它应该有更原始的资料,比如Unix时期的,BSD的,甚至有可能有Posix标准。顺着这个思路我通过 ls的wikipedia 找到了ls在BSD和Unix时代的文档:
这两个文档都有对Long-format的解释。比如FreeBSD的解释(开头一部分)是:
The Long Format If the -l option is given, the following information is displayed for each file: file mode, number of links, owner name, group name, MAC label, number of bytes in the file, abbreviated month, day-of-month file was last modified, hour file last modified, minute file last modified, and the pathname.
Posix给出的(部分)解释是:
If the -l option is specified, the following information shall be written for files other than character special and block special files: "%s %u %s %s %u %s %s
", <file mode>, <number of links>, <owner name>, <group name>, <size>, <date and time>, <pathname>
这里面都清楚的说明了单独的ls的-l选项的格式(非固定描述),让我们结合Linux下ls -l的一个普通输出来看一下:
$ ls -l img -rw-rw-r--. 1 test test 1073741824 Nov 26 11:40 img <file mode> <number of links> <owner name> <group name> <size> <data and time> <pathname>
可以看到Linux下的这行输出和Posix的标准完全对应上了,当然FreeBSD的描述中还说有MAC label(强制访问控制标签)等内容,但是这个不是默认输出要求,我们就关注我们现在看到的。
所以题目问的第二个数字是什么,也就是文件mode之后的那个数字,解释上说是"number of links",直译过来就是“链接数”,这个对于初次看到的人来说可能有些莫不着头脑。让我们继续往下看。
在GNU/Linux系统中,ls命令来源于一个叫coreutils的项目,通过项目名称我们就可以知道这个项目在系统中的位置有多么重要了,很多我们常用的核心工具都在这里,比如ls。
这是GNU的项目,我们尝试从GNU获取这个项目的最新代码(2022-03-25)
# git clone https://git.savannah.gnu.org/git/coreutils.git # cd coreutils # ls src/ls.c src/ls.c
现在我们就拿到了ls.c的原代码,下面没有别的,开始读代码吧!通过阅读和调试:
$ gdb src/ls (gdb) set args -l AUTHORS 3421 err = do_stat (full_name, &f->stat); (gdb) 0x0000000000407d7c in do_stat (st=<optimized out>, name=<optimized out>) at src/ls.c:1201 1201 return do_statx (AT_FDCWD, name, st, 0, calc_req_mask ()); (gdb) bt #0 0x0000000000407d7c in do_stat (st=<optimized out>, name=<optimized out>) at src/ls.c:1201 #1 gobble_file (name=0x7fffffffe168 "AUTHORS", type=type@entry=unknown, command_line_arg=command_line_arg@entry=true, dirname=dirname@entry=0x41aa11 "", inode=0) at src/ls.c:3421 #2 0x0000000000403b91 in main (argc=3, argv=0x7fffffffdd88) at src/ls.c:1764
我们发现ls通过do_statx这个函数获取文件的属性信息,而这个do_statx的代码是这样的:
static int do_statx (int fd, char const *name, struct stat *st, int flags, unsigned int mask) { struct statx stx; bool want_btime = mask & STATX_BTIME; int ret = statx (fd, name, flags | AT_NO_AUTOMOUNT, mask, &stx); if (ret >= 0) { statx_to_stat (&stx, st); /* Since we only need one timestamp type, store birth time in st_mtim. */ if (want_btime) { if (stx.stx_mask & STATX_BTIME) st->st_mtim = statx_timestamp_to_timespec (stx.stx_btime); else st->st_mtim.tv_sec = st->st_mtim.tv_nsec = -1; } } return ret; }
可以看到它使用了一个典型的系统调用来获取文件的属性,那就是statx。其实这里是因为我当前的系统支持statx系统调用,这个系统调用在老的系统上是不支持的,在不支持的时候ls会使用老的stat系统调用。
好了,不管是statx还是stat系统调用,我们去看一下它的文档吧:
int statx(int dirfd, const char *pathname, int flags, unsigned int mask, struct statx *statxbuf); DESCRIPTION This function returns information about a file, storing it in the buffer pointed to by statxbuf. The returned buffer is a structure of the following type: struct statx { __u32 stx_mask; /* Mask of bits indicating filled fields */ __u32 stx_blksize; /* Block size for filesystem I/O */ __u64 stx_attributes; /* Extra file attribute indicators */ __u32 stx_nlink; /* Number of hard links */ ...
上面我们说ls -l输出的第二个数字"number of links"就是对应的stx_nlink这个成员。(stat系统调用也有一个struct stat{}结构体,也有一个st_nlink的成员。我们针对statx的stx_nlink继续来展开,stat与此同理)。我们在使用statx系统调用的时候会初始化一个没有有效值的struct statx结构体,然后让statx系统调用通过内核获取文件的属性后把有效值填入这个结构体并返回给用户。
下面我们就到内核中去看一下statx系统调用,它被定义在fs/stat.c中:
SYSCALL_DEFINE5(statx, int, dfd, const char __user *, filename, unsigned, flags, unsigned int, mask, struct statx __user *, buffer) { return do_statx(dfd, filename, flags, mask, buffer); }
在当前的Linux v5.17-rc8中它通过do_statx()->vfs_statx()->vfs_getattr()->vfs_getattr_nosec()的顺序调用到文件所属文件系统的具体getattr方法: inode->i_op->getattr。不同的文件系统会有不同的实现,我们以XFS文件系统为例看一下实现:
STATIC int xfs_vn_getattr( struct user_namespace *mnt_userns, const struct path *path, struct kstat *stat, u32 request_mask, unsigned int query_flags) { struct inode *inode = d_inode(path->dentry); ... stat->size = XFS_ISIZE(ip); stat->dev = inode->i_sb->s_dev; stat->mode = inode->i_mode; stat->nlink = inode->i_nlink; ... ...
嗯……看来XFS直接使用的当前文件在内存中的inode结构的i_nlink,我们还是回到VFS层,看到inode的i_nlink在VFS层的定义是:
struct inode { ... union { const unsigned int i_nlink; unsigned int __i_nlink; }; ... }
在一个联合体里两个变量,一个用于只读(i_nlink),一个用于写的情况(__i_nlink)。那么我们看一下XFS在什么情况会修改__i_nlink。我们发现XFS修改__i_nlink主要通过xfs_bumplink (自增), xfs_droplink(自减)以及稍微原始一些的set_nlink(直接设置数值)函数(当然还有别的)。
我们首先发现目录和文件初始nlink数值是不一样的:
error = xfs_dialloc(&tp, dp->i_ino, mode, &ino); if (!error) error = xfs_init_new_inode(mnt_userns, tp, dp, ino, mode, is_dir ? 2 : 1, rdev, prid, init_xattrs, &ip);
如果创建的是一个目录,那么初始nlink值是2("."和".."后面解释),如果创建的是一个全新的普通文件,那初始值是1。
除此之外有两处比较典型的导致nlink值增长的地方(也有别的地方),分别是xfs_create()和xfs_link()。 xfs_link()很好理解,它就是我们熟悉的link系统调用:
link() creates a new link (also known as a hard link) to an existing file.
通常理解为创建硬链接。所以当为一个已知文件创建一个硬链接的时候,这个文件所对应的inode的nlink要增加:
int xfs_link( xfs_inode_t *tdp, xfs_inode_t *sip, struct xfs_name *target_name) { ... xfs_bumplink(tp, sip); ... }
而另一个xfs_create()它对应的其实是inode operations中的create/mkdir//mknod方法,这些方法一看就知道是用来创建新“文件”的。所以xfs_create的作用是在一个已知目录下创建一个新的文件/目录等。
int xfs_create( struct user_namespace *mnt_userns, xfs_inode_t *dp, struct xfs_name *name, umode_t mode, dev_t rdev, bool init_xattrs, xfs_inode_t **ipp) { int is_dir = S_ISDIR(mode); ... ... if (is_dir) { error = xfs_dir_init(tp, ip, dp); if (error) goto out_trans_cancel; xfs_bumplink(tp, dp); } ... ... }
但是它比较特殊的是,只有当要创建的是个目录的时候,才会增加所在目录的nlink。举例来说就是如果执行"mkdir /mnt/test/dir1/dir2"成功,那么会增加dir1这个inode的nlink数值,但是如果执行"touch /mnt/test/dir1/file"则不会。
所以我们发现对于XFS来说,两种情况影响nlink的数值,一种是增加或减少文件的硬链接数量,另一种是增加或减少一级子目录的数量。换句通俗点的话说就是:文件的nlink表示文件在当前文件系统内的硬链接数目,目录的nlink表示目录的一级子目录有多少(Linux也不支持目录的硬链接,具体原因可以参考我另外一个回答:linux为什么不能硬链接目录?)。
但是这里需要注意一点,并没有标准定义nlink到底指什么,特别是对于目录的情况。所以不同的系统或者不同的文件系统会有各自的实现,我们上面只是以Linux下的XFS为例得出的结论。当然实际上Linux上大部分我们常见的文件系统都遵循这个行为,我只是说我们不能将其作为恒定不变的准则,如果一个文件系统是这样那就是这样,如果不是这样那就要另外分析。
为了验证我们的推论,我们做一些简单的操作来看看。
首先是创建空目录和空文件和一个字符设备文件:
# ls # mkdir dir # touch file # mknod mynull c 1 3 # ls -l total 0 drwxr-xr-x. 2 root root 6 Mar 26 02:49 dir -rw-r--r--. 1 root root 0 Mar 26 02:49 file crw-r--r--. 1 root root 1, 3 Mar 26 02:50 mynull
可以看出目录的初始nlink确实是2,普通文件和字符设备文件的初始nlink是1。
然后我们给文件创建硬链接:
# ln mynull mynull2 # ln file file2 # ln file file3 # ls -l total 0 drwxr-xr-x. 2 root root 6 Mar 26 02:49 dir -rw-r--r--. 3 root root 0 Mar 26 02:49 file -rw-r--r--. 3 root root 0 Mar 26 02:49 file2 -rw-r--r--. 3 root root 0 Mar 26 02:49 file3 crw-r--r--. 2 root root 1, 3 Mar 26 02:50 mynull crw-r--r--. 2 root root 1, 3 Mar 26 02:50 mynull2
可以看到普通文件由于增加了两个硬链接,所以nlink变为3。设备文件由于增加了一个硬链接所以nlink变成了2。
下面我们给目录下创建一些子文件(不创建目录):
# touch dir/file{1,2,3} # ls dir file1 file2 file3 # ls -l total 0 drwxr-xr-x. 2 root root 45 Mar 26 03:00 dir ...
可以看到目录的nlink并没有变化,还是初始的2。那么下面我创建几个子目录试试:
# mkdir dir/dir{1,2,3,4} # ls dir dir1 dir2 dir3 dir4 file1 file2 file3 # ls -l total 0 drwxr-xr-x. 6 root root 125 Mar 26 03:02 dir ...
创建了4个一级子目录后,dir的nlink也跟着加4,变成了6,结果符合我们上面的分析。
所以我们给出一个象征性的结论:ls -l输出的第二个域是“number of links”(简称nlink),它是ls命令通过stat()或statx()系统调用获取到的inode的nlink数值。在Linux的一些常见文件系统上,这个数值对于文件来说表示文件的硬链接数,对于目录来说表示目录的一级子目录的个数。但是需要特别提醒的是目前并没有标准定义nlink的具体含义,特别是对于类似目录这样的特殊情况,所以不要将此作为放之四海皆准的结论,我们要保持开放的态度,具体问题具体分析。