文件系统
inode
其本质为结构体,存储文件的属性信息。如:权限、类型、大小、时间、用户、盘块位置(指向内容存储的磁盘位置)、引用计数等。也叫做文件属性管理结构,大多数的inode都存储在磁盘上。
近期使用的inode会被缓存到内存中。创建硬链接时,这些链接有相同的inode(不同的dentry),指向的磁盘存储空间也一致。
dentry
目录项,其本质依然是结构体,重要成员变量有两个:文件名、inode号(关联到inode中),而文件内容保存在磁盘盘块中。
函数stat、fstat、statat和lstat
#include int stat(const char *restrict pathname, struct stat *restrict buf);int fstat(int fd, struct stat *buf);int lstat(const char *restrict pathname, struct stat *restrict buf);int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);成功返回0;失败返回-1。
给出pathname,stat函数返回与此命名文件有关的信息结构。fstat获得已在描述符fd上打开的文件的有关信息。lstat函数类似于stat,但是当命名的文件是一个符号链接时,lstat返回该符号链接的有关信息,而不是由该符号链接引用的文件的信息(stat会有符号穿透,lstat不会)。
fstatat函数为一个相对于当前打开目录(由fd参数指向)的路径名返回文件统计信息。flag参数控制着是否跟随着一个符号链接。当flag设置为AT_SYMLINK_NOFOLLOW时,fstatat不会跟符号链接,而是返回符号链接本身的信息,否则默认情况下,返回的是符号链接所指向的实际文件的信息。如果fd的值是AT_FDCWD,并且pathname参数是一个相对路径名,fstatat会计算相对于当前目录的pathname参数。如果pathname是一个绝对路径,fd参数就会被忽略,这种情况下,根据flag的取值,fstatat的作用和stat或lstat一样。
buf是一个指针,函数来填充buf指向的结构,其基本形式是:
struct stat { mode_t st_mode; /* file type /* i-node number (serial number) */ dev_t st_dev; /* ID of device containing file */ dev_t st_rdev; /* device ID (if special file) */ nlink_t st_nlink; /* number of links */ uid_t st_uid; /* user ID of owner */ gid_t st_gid; /* group ID of owner */ off_t st_size; /* total size, in bytes, for regular files */ struct timespec st_atime; /* time of last access */ struct timespec st_mtime; /* time of last modification */ struct timespec st_ctime; /* time of last file status change */ blksize_t st_blksize; /* blocksize for filesystem I/O */ blkcnt_t st_blocks; /* number of disk blocks allocated */};
timespec结构类型按照秒和纳秒定义了时间,至少包括下面两个字段:
time_t tv_sec;long tv_nsec;
文件类型
文件类型信息都包含在stat结构的st_mode成员中。可以使用一些宏来确定文件类型:这些宏的参数都是stat结构中的st_mode成员。
S_ISREG(m) 普通文件S_ISDIR(m) 目录文件S_ISBLK(m) 块特殊文件S_ISCHR(m) 字符特殊文件S_ISFIFO(m) 管道或FIFOS_ISSOCK(m) 套接字S_ISLINK(m) 符号链接
早起的UNIX版本不提供S_ISxxx宏,需要将st_mode与屏蔽字S_IFMT进行逻辑与运算,然后与名为S_IFxxx的常量相比较。
设置用户ID和设置组ID
与一个进程相关联的ID有6个或更多:
实际用户ID实际组ID有效用户ID有效组ID附属组ID保存的设置用户ID保存的设置组ID
- 实际用户ID和实际组ID标识我们究竟是谁。这两个字段在登录时取自口令文件中的登录项。通常在一个登录会话期间这些值并不改变。
- 有效用户ID、有效组ID以及附属组ID决定了我们的文件访问权限。
- 保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有效用户ID和有效组ID的副本。
通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID。每个文件有一个所有者和组所有者,所有者由stat结构中的st_uid指定,组所有者则由st_gid指定。
当执行一个程序文件时,进程的有效用户ID通常就是实际用户ID,但是可以在st_mode中设置一个特殊标志,其含义是“当执行此文件时,将进程的有效用户ID设置为文件所有者的用户ID(st_uid)”。与此类似,在st_mode中可以设置另一位,将执行此文件的进程的有效组ID设置为文件的组所有者ID(st_gid)。
例如,若文件所有者是超级用户,而且设置了该文件的设置用户ID位,那么当该程序文件由一个进程执行时,该进程具有超级用户权限,不管执行此文件的进程的实际用户ID是什么。
文件访问权限
st_mode值也包含了对文件的访问权限位。所有文件类型(目录、字符特别文件等)都有访问权限。每个文件有9个权限位,可分为3类。
S_IRUSR 用户读S_IWUSR 用户写S_IXUSR 用户执行S_IRGRP 组读S_IWGRP 组写S_IXGRP 组执行S_IROTH 其他读S_IWOTH 其他写S_OXOTH 其他执行
3类访问权限以各种方式由不同的函数使用:
- 我们用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。这就是为什么对于目录其执行权限位常被称为搜索位的原因。注意,对于目录的读权限和执行权限的含义是不同的。读权限允许我们读目录,获得在该目录中所有文件名的列表,但是没有执行权限,无法进入文件夹,即对该目录的执行权限使我们可通过该目录(也就是搜索该目录,寻找一个特定的文件名)。
- 对于一个文件的读权限决定了我们是否能打开现有文件进行读操作。
- 对于一个文件的写权限决定了我们是否能打开现有文件进行写操作。
- 为了在open函数中对一个文件指定O_TRUNC标志,必须对该文件具有写权限。
- 为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。
- 为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限。对该文件本身不需要有读写权限。
- 如果用exec函数中的任何一个执行某个文件,都必须对该文件具有执行权限。该文件还必须是一个普通文件。
进程每次打开、删除或删除一个文件,内核就进行文件访问权限测试:
新文件和目录的所有权
新文件的用户ID设置为进程的有效用户ID。组ID可以是进程的有效组ID或它所在目录的组ID。
函数access和faccessat
当用open函数打开一个文件时,内核以进程的有效用户ID和有效组ID为基础执行其访问权限测试。有时,进程也希望按其实际用户ID和实际组ID来测试其访问能力。例如,当一个进程使用设置用户ID或设置组ID功能作为另一个用户(或组)运行时,就可能有这种需要。
即使一个进程可能已经通过设置用户ID以超级用户权限运行,它仍可能想验证其实际用户能否访问一个给定的文件。access和faccessat函数是按实际用户ID和实际组ID进行访问权限测试的。
#include int access(const char *pathname, int mode);int faccessat(int fd, const char *pathname, int mode, int flag);若成功,返回0;若出错,返回-1。
如果测试文件是否已经存在,mode就为F_OK;否则,mode是常量的按位或:
R_OK 测试读权限W_OK 测试写权限X_OK 测试执行权限
faccessat函数与access函数在下面两种情况下是相同的:一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。否则,faccessat计算相对于打开目录(由fd参数指向)的pathname。
flag参数可以用于改变faccessat的行为,如果flag设置为AT_EACCESS,访问检查用的是调用进程的有效用户ID和有效组ID,而不是实际用户ID和实际组ID。
函数umask
#include mode_t umask(mode_t cmask);
umask函数为进程设置文件模式创建屏蔽字,并返回之前的值。cmask是由9个常量(S_IRUSR、S_IWUSR等)中的若干个按位或构成的。在创建屏蔽字中为1的位,在文件权限中的相应位一定被关闭。
当编写创建新文件的程序时,如果想确保指定的访问权限位已经激活,就必须在进程运行时修改umask值,否则当进程运行时,有效的umask值可能关闭权限位。更改进程的文件模式创建屏蔽字并不影响其父进程(常常是shell)的屏蔽字。
可以设置umask值以控制他们所创建文件的默认权限,该值表示成八进制数,一位表示一种要屏蔽的权限。
shell支持符号形式的umask命令,与八进制格式不同,符号格式指定许可的权限而非拒绝的权限。
umask -S
函数chmod、fchmod和fchmodat
#include int chmod(const char *pathname, mode_t mode);int fchmod(int fd, mode_t mode);int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
chmod函数在指定的文件上进行操作,而fchmod函数则对已打开的文件进行操作。fchmodat函数与chmod函数在下面两种情况下是相同的:一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。否则,fchmodat计算相对于打开目录(由fd参数指向)的pathname。flag参数可以用于改变fchmodat的行为,当设置了AT_SYMLINK_NOFOLLOW标志时,fchmodat并不会跟随符号链接。
为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限。参数mode是常量的按位或:
S_ISUID 执行时设置用户IDS_ISGID 执行时设置组IDS_ISVTX 保存正文(粘着位)S_IRWXU 所有者读写执行S_IRUSR 所有者读S_IWUSR 所有者写S_IXUSR 所有者执行S_IRWXG 组读写执行S_IRGRP 组读S_IWGRP 组写S_IXGRP 组执行S_IRWXO 其他读写执行S_IROTH 其他读S_IWOTH 其他写S_OXOTH 其他执行
chmod("foo", (statbuf.st_mode int fchown(int fd, uid_t owner, gid_t group);int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag);int lchown(const char *pathname, uid_t owner, gid_t group);
chown函数可用于更改文件的用户ID和组ID。如果两个参数owner或group中的任意一个是-1,则对应的ID不变。
在符号链接情况下,lchown和fchownat(设置了AT_SYMLINK_NOFOLLOW标志)更改符号链接本身的所有者,而不是该符号链接所指向的文件的所有者。
文件长度
stat结构成员st_size表示以字节为单位的文件的长度。此字段只对普通文件、目录文件和符号链接有意义。对于普通文件,其文件长度可以是0,在开始读这种文件时,将得到文件结束(end-of-file)指示。对于目录,文件长度通常是一个数(如16或512)的整倍数。对于符号链接,文件长度是在文件名中的实际字节数,注意,因为符号链接文件长度总是由st_size指示,所以它并不包含通常的C语言用作字符串结尾的null字节。
文件中的空洞
空洞是由所设置的偏移量超过文件尾端,并写入了某些数据后造成的。
ls -l core-rw-r--r-- l sar 8483248 Nox 18 12:18 coredu -s core272 core
文件core的长度稍稍超过8MB,而du命令报告该文件所使用的的磁盘空间总量是272个512字节块(即139264字节),此文件有很多空洞。
对于没有写过的字节位置,read函数读到的字节是0.如果使用程序复制这个文件,那么所有的空洞都会被填满,其中所有实际数据字节皆填写为0。
文件截断
#include int truncate(const char *pathname, off_t length);int ftruncate(int fd, off_t length);
将一个现有文件长度截断为length,如果该文件以前的长度大于length,则超过length以外的数据就不再能访问。如果以前的长度小于length,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0(也就是可能在文件中创建了一个空洞)。
文件系统
如果两个目录项dentry指向同一个inode节点,(每个inode中都有一个链接计数,其值是指向该inode的目录项数)。只有当链接计数减少至0时,才可删除该文件,也就是可以释放该文件占用的数据块。在stat结构中,链接计数包含在st_nlink成员中。这种链接类型称为硬链接。
另外一种链接称为符号链接(软链接),符号链接文件的实际内容(在数据块中)包含了该符号链接所指向的文件的名字。
inode中包含了文件有关的所有信息:文件类型、文件访问权限位、文件长度和指向文件数据块的指针等。stat结构中的大多数信息都取自inode。只有两项数据存放在目录项dentry中:文件名和inode号。
因为目录项中的inode编号指向同一文件系统中的相应inode,一个目录项不能指向另一个文件系统的inode。这就是ln(构造一个指向一个现有文件的新目录项)不能跨越文件系统的原因。
当在不更换文件系统的情况下为一个文件重命名时,该文件的实际内容并未移动,只需构造一个指向指向现有inode的新目录项,并删除老的目录项。链接计数不会改变。
对于目录文件,任何一个叶目录(不包含任何其他目录的目录)的链接计数总是2,来自于命名该目录的目录项以及在该目录中的.项。注意,在父目录中的每一个子目录都使该父目录的链接计数增加1来自于..。
函数link、linkat、unlink、unlinkat和remove
任何一个文件可以有多个目录项指向其inode。创建一个指向现有文件的链接(硬链接)的方法是使用link函数或linkat函数。
#include int link(const char *existingpath, const char *newpath);int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);若成功,返回0;若出错,返回-1。
两个函数创建一个新目录项newpath,它引用现有文件existingpath。如果newpath已经存在,则返回出错。函数只创建newpath中的最后一个分量,路径中的其他部分应当已经存在。
对于linkat函数,现有文件是通过efd和existingpath参数指定的,新的路径名是通过nfd和newpath参数指定的。默认情况下,如果两个路径名中的任一个是相对路径,则它需要通过相对于对应的文件描述符进行计算。如果两个文件描述符的任一个设置为AT_FDCWD,那么相应的路径名(如果它是相对路径)就通过相对于当前目录进行计算。如果任一路径名是绝对路径,相应的文件描述符参数就会被忽略。
当现有文件是符号链接时,由flag参数来控制linkat函数是创建指向现有符号链接的链接还是创建指向现有符号链接所指向的文件的连接。如果在flag参数中设置了AT_SYMLINK_FOLLOW标志,就创建指向符号链接目标的连接,如果这个标志被清除了,则创建一个指向符号链接本身的链接。
为了删除一个现有的目录项,可以调用unlink函数。
#include int unlink(const char *pathname);int unlinkat(int fd, const char *pathname, int flag);若成功,返回0;若出错,返回-1。
这两个函数删除目录项,并将由pathname所引用文件的链接计数减1,如果对该文件还有其他链接,则仍可通过其他链接访问该文件的数据,如果出错,则不对该文件做任何更改。
只有当链接计数达到0时,该文件的内容才可被删除,无目录项对应的文件,将会被操作系统择机释放。另一个条件也会阻止删除文件的内容——只要有进程打开了该文件,其内容也不能删除。关闭一个文件时,内核首先检查打开该文件的进程个数,如果这个计数达到0,内核再去检查其链接计数,如果计数也是0,那么就删除该文件的内容。
如果pathname参数是相对路径名,那么unlinkat函数计算相对于由fd文件描述符参数代表的目录的路径名。如果fd参数设置为AT_FDCWD,那么通过相对于调用进程的当前工作目录来计算路径名。如果pathname参数是绝对路径名,那么fd参数被忽略。
flag参数给出了一种方法,使调用进程可以改变unlinkat函数的默认行为。当AT_REMOVEDIR标志被设置时,unlinkat函数可以类似于rmdir一样删除目录。如果这个标志被清除,unlinkat与unlink执行同样的动作。
unlink的特性经常被程序用来确保即使是在程序崩溃时,它所创建的临时文件也不会遗留下来。进程用open或creat创建一个文件,然后立即调用unlink,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时(此时,内核关闭该进程所打开的全部文件),该文件的内容才被删除。
如果pathname是符号链接(软链接),那么unlink删除该符号链接,而不是删除由该链接所引用的文件。给出符号链接名的情况下,没有一个函数能删除由该链接所引用的文件。
也可以用remove函数解除对一个文件或目录的链接,对于文件,remove的功能与unlink相同,对于目录,remove的功能与rmdir相同。
#include int remove(const char *pathname);
函数rename和renameat
文件或目录可以用rename函数或者renameat函数进行重命名。
#include int rename(const char *oldname, const char *newname);int renameat(int oldfd, const char *oldname, int newfd, const char *newname);若成功,返回0;若出错,返回-1。
oldfa或newfd参数都能设置成AT_FDCWD,此时相对于当前目录来计算相应的路径名。
符号链接
符号链接是对一个文件的间接指针,与硬链接有所不同,硬链接直接指向文件的inode。引入符号链接的原因是为了避免硬链接的一些限制。
- 硬链接通常要求链接和文件位于同一文件系统中。
- 只有超级用户才能创建指向目录的硬链接。
当使用以名字引用文件的函数时,应当了解该函数是否处理符号链接。也就是该函数是否跟随符号链接到达它所链接的文件。如果该函数具有处理符号链接的功能,则其路径名参数引用由符号链接指向的文件。否则,一个路径名参数引用链接本身,而不是由该链接指向的文件。当前所接触的函数中,lchown、lstat、readlink、remove、rename、unlink都不跟随符号链接。
用open打开文件时,如果传递给open函数的路径名指定了一个符号链接,那么open跟随此链接到达所指定的文件。若此符号链接所指向的文件并不存在,则open返回出错,表示它不能打开该文件。
创建和读取符号链接
可以用symlink或symlinkat函数创建一个符号链接:
#include int symlink(const char *actualpath, const char *sympath);int symlinkat(const char *actualpath, int fd, const char *sympath);
函数创建一个指向actualpath的新目录项sympath。在创建此符号链接时,并不要求actualpath已经存在。symlinkat函数与symlink函数类似,但sympath参数根据相对于打开文件描述符引用的目录(有fd参数指定)进行计算。如果sympath参数指定的是绝对路径或者fd参数设置为AT_FDCWD值,那么symlinkat就等同于symlink函数。
因为open函数跟随符号链接,所以需要有一种方法打开该链接本身,并读链接中的名字:
#include ssize_t readlink(const char *pathname, char *restrict buf, size_t bufsize);ssize_t readlinkat(int fd, const char *restrict pathname, char *buf, size_t bufsize);
如果函数成功执行,则返回读入buf的字节数,在buf中返回的符号链接不以null字节终止。
文件的时间
每个文件维护三个时间字段:
st_atim 文件数据的最后访问时间 read ls -ust_mtim 文件数据的最后修改时间 write lsst_ctim inode状态的最后更改时间 chmod、chown ls -c
修改时间是文件内容最后一次被修改的时间。状态更改时间是该文件的inode最后一次被修改的时间。状态更改如:更改文件访问权限、更改用户ID、更改链接数等,但它们并没有更改文件的实际内容。因为inode中的所有信息都是与文件的实际内容分开存放的,所以,需要记录文件数据修改时间以外,还需要记录状态更改时间。
函数futimens、utimensat和utimes
一个文件的访问和修改时间可以用一下几个函数更改。futimens和utimensat函数可以指定纳秒级精度的时间戳。
#include int futimens(int fd, const struct timespec times[2]);int utimensat(int fd, const char *path, const struct timespec times[2], int flag);若成功,返回0;若出错,返回-1。
这两个函数的times数组参数的第一个元素包含访问时间,第二个元素包含修改时间。这两个时间值是日历时间,从1970年1月1日以来所经历的秒数。
时间戳可按下列4种方式之一进行指定:
函数mkdir、mkdirat和rmdir
用mkdir和mkdirat函数创建目录,用rmdir删除目录。
#include int mkdir(const char *pathname, mode_t mode);int mkdirat(int fd, const char *pathname, mode_t mode);若成功,返回0;若出错,返回-1。
对于目录通常至少要设置一个执行权限位,以允许访问该目录的文件名。
用rmdir函数可以删除一个空目录,空目录是只包含.和..这两项的目录。
#include int rmdir(const char *pathname);若成功,返回0;若出错,返回-1。
如果调用此函数使目录的链接计数成为0,并且也没有其他进程打开此目录,则释放由此目录占用的空间。
函数chdir、fchdir和getcwd
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点。进程调用chdir或fchdir函数更改当前工作目录。
#include int chdir(const char *pathname);int fchdir(int fd);
函数getcwd获取进程当前工作目录的绝对路径。
#include char *getcwd(char *buf, size_t size);
文件、目录rwx权限
目录也是文件,其文件内容是该目录下所有子文件的目录项dentry。
读文件:cat、more、less...
写文件:vi、>...
执行文件:./fname
读目录项:ls、tree...
删除、创建、修改文件:mv、touch、mkdir...
搜索路径名:cd
目录设置粘着位,若有w权限,创建权限不受影响,修改、删除只能由root、目录所有者、文件所有者操作。
读目录
对某个目录具有访问权限的任一用户都可以读该目录,但是只有内核才能写目录。一个目录的写权限位和执行权限位决定了在该目录中能否创建新文件以及删除文件,并不表示能否写目录本身。
#include DIR *opendir(const char *pathname);DIR *fdopendir(int fd);若成功,返回指针;若出错,返回NULLstruct dirent *readdir(DIR *dp);若成功,返回指针;若在目录尾或出错,返回NULLvoid rewinddir(DIR *dp); 回卷目录读写位置至起始int closedir(DIR *dp);若成功,返回0;若出错,返回-1long telldir(DIR *dp);返回与dp关联的目录中的当前位置,获取目录读写位置void seekdir(DIR *dp, long loc); 修改目录读写位置
dirent结构与实现有关,定义至少包括成员:
ino_t d_ino;char d_name[]; 以NULL结尾的文件名
d_name项的大小没有被指定,文件名以null字节结束,所以在头文件中如何定义数组d_name无关系,数组大小不表示文件名的长度。