1. Introduction
前面一章主要介绍的都是如何操作文件,打开,读取,写入。本章是学习文件系统所提供的其他特性,以及UNIX系统是如何符号链接以及UNIX文件系统的结构。
2. Stat,fstat,fstatat and lstat Functions
这里的四个函数都是用于返回文件的相关信息的。
stat是返回文件名为pathname的文件的相关信息。fstat则返回的是参数fd
的相关信息,这是一个已经打开的文件的文件描述符。lstat函数和stat有些类似,但是它的参数pathname是一个符号链接,lstat返回的是这个符号链接的相关内容,而不是符号链接所指向的文件。fstatat的用法比较复杂,不过功能是差不多的。
上面函数中的buf是一个结构体指针,struct buf
包括了一个文件的大多数信息,结构如下,其中的struct timespec
是一个纳秒和秒的结构体,比较简单,就不再贴了。mode_t则是表示文件的类型,接下来会说到。对于stat结构使用最多的应该是ls -l程序了。
3. File Types
之前只介绍了两种文件类型,即普通文件和目录,虽然大多数unix文件不外乎都是这两类,下面系统地介绍unix文件的类型。
- 普通文件。这是最常见的类型。对于内核而言,unix内核不会区分一个文件是文本类型还是二进制的文件,如何去解释文件内容由程序自己去决定。
- 目录文件。目录也和文件一样,只不过它的数据是其他文件的文件名,以及指向这些相关信息的指针。
- Block special file。 以固定单元大小并且提供了缓存,硬盘就是这类文件。
- 字符设备。A type of file providing unbuffered I/O access in variable-sized units to devices.
- FIFO。用于进程之间通信的。
- Socket。用于网络通信的file,当然也可以是同一台主机上的socket通信。
- Symbolic link。 指向其他文件的一种文件。
struct stat
中的mode_t
决定了文件的类型。用于判断文件类型的宏定义如下:
4. Set-User ID and Set-Group-ID
每一个进程有六个或者多个ID,如下图:
- user ID 和group ID用于表明当前进程的身份,它是由password file中获取的,大部分情况下登陆后这些东西都是不能被修改的,但是superuser还是可以修改的。
- effective user ID, effective group ID, and supplementary group IDs 则是决定了文件的访问权限,后面会说到。
- saved set-user-ID 和 saved set-group-ID 是 effective user ID和 effective group ID 的copy,本章暂时用不到。
5. File Access Permissions
struct stat
结构中的st_mode
中同样的还包含了文件的访问权限,前面提到的所有文件都有访问权限,包括目录,设备。控制权限的一共有9个bit,如下图:
user表示的是文件的归属者,可以通过chmod来修改文件的权限。至于,内核是如何使用各个控制权限的bit是如何影响操作的,原文写的很全面,需要的时候在看书吧。
6. access and faccessat Functions
为了测试我们是否有权限去访问一个文件,那么可以使用access或者faccessat函数,如下:
#include <unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
参数mode可以是F_OK(用于测试文件是否存在),或者是下图的标志位,多个标志位可以通过OR来聚合:
当path是绝对路径或者是fd 为 AT_FDCWD的时候,faccessat的行为和access相类似。其他情况下,pathname是相对于文件描述符fd的相对路径。
7. umask Function
我们可以创建一个mask,来设置当前进程所创建文件的时候,该文件的权限。这个函数为unmask,unmask会创建一个file mode creation mask,函数如下:
#include <sys/stat.h>
mode_t umask(mode_t cmask);
open的第三个参数是用于空所创建的文件权限的,该参数和umask搭配在一起形成了文件最后的权限,即在cmask中被设置的bit,会将open参数中的mode所对应的bit取消置位。
Any bits that are on in the file mode creation mask are turned off in the file’s mode
测试程序如下:
#include "apue.h"
#include <fcntl.h>
#define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)
int main(void)
{
umask(0);
if (creat("foo", RWRWRW) < 0)
err_sys("creat error for foo");
umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); // 将这四个标志位取消设置
if (creat("bar", RWRWRW) < 0) // 因为umask,RWRWRW就变成了RW
err_sys("creat error for bar");
exit(0);
}
如果需要使用open来创建文件,那么我们必须使用umask来确保访问权限被正确的设置,如上述代码中的umask(0)
没有被设置,那么文件创建的权限就不是RWRWRW
。而且,子父进程的file mode creation mask是相互独立的。我们可以使用umask命令来修改用shell创建的文件的访问权限。通常umask的默认值是022,即S_IWGRP | S_IWOTH
。其他可行的值如下,可以通过OR来进行组合:
umask命令默认情况下输出的是八进制数, 可以使用umask -S
来转为字符形式:
umask 022 // 将umask设置为022
umask -S // 在022情况下的umaks,文件的权限为rwx rx rx,但是这还取决于open的参数,如果是rwrwrw,那么得到的就是rw r r
8. chmod,fchmod,and fchmodat Functions
这三个函数用于修改文件的权限,函数签名如下:
#include <sys/stat.h>
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和前面那些带有at的函数相类似,如果pathname是绝对路径或者fd是AT_FDCWD,并且path是相对路径,这两种情况下fchmodat和chmod相类似的。或者是,pathname为相对路径,fd为它的父目录的文件描述符。权限如下图,总体和前面所介绍的9个bit文件权限差不多:
9. Sticky Bit
上面有一个标志位S_ISVTX,它的历史比较有意思。在早期的unix系统还没有按需分配页(page)的时候,这个标志位被称为sticky bit,它主要的作用用于可执行文件(ELF),在第一次被加载的时候,可执行文件的代码(text段)被复制到了一块区域,那么接下来加载的时候就会更快,因为本来的话elf文件在硬盘中并不一定是连续的,加载文件的时候需要额外的时间。sticky bit通常用于那些比较常用的软件,如文本编辑器,C 编译器。但是随着软硬件技术的发展,这个标志位现在用的少多了。
10. chown, fchown, fchownat, and lchown Functions
chown函数用于修改该文件的userid和groupid,如果传入的参数为-1,那表示这个id不会被修改。函数签名如下:
#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
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);
这四个函数效果相类似的,除非是在symbolic link中,在这个情况中lchown和fchownat会修改symbolic link本身的owners,而不是symbolic link指向的文件。fchownat和前面带有at的函数相类似,不再赘述。如果fchownat中的flag = AT_SYMLINK_NOFOLLOW,那么就和fchown相类似,否则的话就和chown相类似。BSD系统下只能superuser可以修改owners,System V则是文件的归属者就可以修改owner。
11. File Size
struct stat
中的st_size
表示了文件大小。这个成员只有给普通文件,目录,符号链接有用。对于普通文件来说,大小为0是允许的,对于符号链接,file size表示的是文件名的长度,例子如下:
lrwxrwxrwx 1 root 7 Sep 25 07:14 lib -> usr/lib
7字节表示的是usr/lib
。大多数现代的unix系统的struct stat
还包括st_blksize
和st_blocks
。分别表示文件系统所使用的IO块大小以及实际上一个块占据了多少硬盘扇区。前面一章已经学过,read的时候提供和st_blksize一样大小的缓冲区有更好的性能。
13. File Truncation
如果需要对文件数据进行截断,那么可以调用truncate函数,函数签名如下:
#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
该函数的作用是将文件截断到length(参数)个字节长度。如果length比现有的文件大小要大,那么truncate可能会增加文件大小。
14. File Systems
UNIX文件系统的经典结构如下图,最上一层的是分区表,一个硬盘被分区表划分为多个分区,每一个分区都有不同的文件系统。cylinder group代表的是硬盘磁道的划分,可以理解为就是一大块硬盘扇区。一个分区中前面部分空间是存放文件系统的元数据(metadata),如文件系统的名称,版本等等,这些内容都会放在super block中。接下来重要的是i-node map,每一个文件都有自己的inode,inode map一般是一个位图,用于表示某个inode是否可用。block map是扇区的位图,用于表示某个扇区是否可用。接着的是inodes 和data blocks,inodes 可以认为是一个数组,就是用来放inode的,再接着的就是data blocks了,这才是实际存放文件数据的地方。
再来看上图:
每一个目录都有一个指向自己inode的指着。两个目录也可以指向同一个i node,因此每一个i node都有一个link count,只有link count == 0的时候,这里的数据才是被丢弃的。删除文件实际上做的就是unlink ,而不是delete。在
struct stat
中,st_nlink
结构就是用于表示link count,这类链接叫做硬链接。也就是,两个directory entry指向了同一个inode,软连接后面会说到。
符号链接(symbolic link)也是一种文件,只不过它的文件内容是它要指向的文件的名字,对于符号链接而言,它的inode type 是S_IFLNK (毕竟硬盘数据只是一些字节,如果inode没有type,那么就不知道如何去解释这些数据了)。
i node存放了一个文件的大部分信息,如类型,大小,访问权限,硬盘扇区在何处等。
stat
命令的大部分内容都是从这里来的。而对于目录来说,它对应的结构体存放的东西就是:文件名和所对应的i-node。所以无论是目录,还是普通文件,它们的文件名都是由目录这个结构体保存的,而不是inode。因为目录内的inode指针是指向同一个文件系统的inode的,所以ln(用于将两个目录项指向同一个inode)不能跨文件系统使用。
15. link, linkat, unlink, unlinkat, and remove Functions
多个目录项可以指向同一个inode,我们可以使用link和linkat来实现这个。重申一遍,目录实际上就是可以简单的认为是一个结构体,link的作用就是将一个字符串(表示文件名)和inode进行绑定。link和linkat的函数签名如下:
#include <unistd.h>
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
link表示将existingpath
所对应的inode与newPath
绑定。linkat和前面at的都很类似,不再多说。linkat的flag参数主要是用于控制当existingpath
是符号链接的时候,如果此时flag为AT_SYMLINK_FOLLOW
,那么就是创建一个directory entry,指向的是符号链接所指向的数据。否则的话,就是创建一个directory entry指向符号链接它本身。
移除一个directory entry,那么使用的就是unlink,如下:
#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd, const char *pathname, int flag);
16.Symbolic Links
符号链接是间接地指向一个文件的,它内部的数据是另外所指向文件的文件名,而硬链接则是直接和inode绑定起来。软连接的目的是解决硬链接只能链接同一个文件系统内的文件这一限制,而且有些文件系统不支持硬链接到目录。对于软连接而言,没有文件系统的限制。当一个函数使用文件名作为参数的时候,它到底是操作它到底是操作Symbolic Links本身还是它所指向的数据,有时候可以用flag=AT_SYMLINK_FOLLOW 来控制。有些函数不支持参数是Symbolic Links,如mkdir,mkfifo,mknod等。
这个知识点面试也比较会考,做一些例子来加深一下。
首先使用ln来创建软硬连接:
touch hello.txt
ln hello.txt hello1.txt
ln -s hello.txt hello-s.txt // -s表明创建是软连接
实验结果图如下,用ls -l
输出。
查看其各自的inode号:
软连接为9个字节,恰好就是hello.txt
的长度,也就是说明软连接文件内的内容就是所指向文件的文件名。它有自己的inode,而硬链接则是创建了两个struct dirent
,它们指向的是相同的inode。
Example
Symbolic Links会带来循环的问题,大多数需要进行文件名查找的函数在这种情况下都会返回errno of ELOOP,使用如下例子来复现:
$ mkdir foo //make a new directory
$ touch foo/a //create a 0-length file
$ ln -s ../foo foo/testdir //create a symbolic link
$ ls -l foo
上面的意思是将foo/testdir指向foo目录,也就是说foo下的子目录指向了foo,就变成循环了。解决办法也很简单,直接unlink即可,因为unlink does not follow a symbolic link(unlink处理的是符号链接本身)。
如果将一个符号链接传给open,但是实际上open所指向的文件并不存在,那么输出的结果会让人难以理解。注意,符号链接是可以指向一个不存在的文件的,可以通过ln -s
来进行实验得到。
如上图,myfile指向一个不存在的文件,但是ls -l
输出的内容却说有这个文件,值得注意。
17. Creating and Reading Symbolic Links
符号链接由symlink或者symlinkat这两个函数来创建。签名如下:
#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
建立一个路径为sympath
,指向actualpath
的符号链接,at函数的逻辑和前面很像不再赘述。因为open它是直接读取符号链接所指向的数据的,所以需要有额外的函数来读取符号链接本身的内容,即readlink和readlinkat,如下:
#include <unistd.h>
ssize_t readlink(const char* restrict pathname, char *restrict buf, size_t bufsize);
ssize_t readlinkat(int fd, const char* restrict pathname, char *restrict buf, size_t bufsize);
这些函数是open,read,write的结合体,readlink将符号链接pathname的数据读取,写入到buf中。
18. File Times
struct stat
中的三个成员分别表示了和文件相关的时间,如下图.虽然在Single UNIX Specification中对时间增加了纳秒,但是实际的实现取决于文件系统。不过我看了下ubuntu确实是由两个秒和纳秒来组成的。
st_mtim和st_ctim的区别是,st_mtim表示的是最后一次对文件修改,即修改了文件内部的数据,st_ctim表示的则是修改了i-node的属性。比如说前面提到的,修改文件的权限,修改文件的userid等,这些操作影响的是st_ctim。st_atim会被系统管理员来使用,如果文件一段时间没有被使用,那么就可以删除了。
ls
命令在带上-l和-t
选线的时候,输出的是文件的修改时间。-u输出的是access time,-c输出的是changed-status time。
20. futimens, utimensat, and utimes Functions
这三个函数用于修改文件的access time和modification time,函数签名如下。通过times[2]
这个数组传递我们要修改的时间,futimens和utimensat用法有些复杂,具体可以参考man page以及书上的描述。一个提及的问题就是,为什么没有函数用于修改st_ctime,是因为调用utime函数就会自动的对st_ctim进行更新。
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);
int utimensat(int fd, const char *path, const struct timespec times[2], int flag);
int utimes(const char *pathname, const struct timeval times[2]);
Note that we are unable to specify a value for the changed-status time, st_ctim—the time the i-node was last changed—as this field is automatically updated when the utime function is called
21. mkdir, mkdirat, and rmdir Functions
通过mkdir 和mkdirat来创建文件夹,通过rmdir来移除文件夹,但是只能是空的文件夹。函数签名如下:
#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
#include <unistd.h>
int rmdir(const char *pathname);
这些函数所创建的目录下只有.
和..
两个东西。一个常见的误区就是将普通文件的权限带入到目录的权限。但是为了能够访问一个目录的内容,一个目录必须有executable bit。rmdir作用是移除空文件夹,而unlink移除的是文件。
在文件系统的底层,目录的数据下面就是一个一个的struct dirent
,无论是子目录,还是一个文件,都占据一个directory entry。rmdir做的应该就是将它所对应的struct dirent
移除。所有的文件都是占据一个dirent可以使用书中的程序来实验:
int
main(int argc, char *argv[])
{
DIR *dp;
struct dirent *dirp;
if (argc != 2)
err_quit("usage: ls directory_name");
if ((dp = opendir(argv[1])) == NULL)
err_sys("can’t open %s", argv[1]);
while ((dirp = readdir(dp)) != NULL)
printf("%s\n", dirp->d_name);
closedir(dp);
exit(0);
}
该程序会输出无论是普通文件还是目录文件。无论是link还是mkdir等创建文件和创建目录操作都会增加一个struct dirent
,虽然没有看到实际的内核代码是怎么实现的。但是我猜rmdir和unlink在程序上有些相似,毕竟只要将所对应的struct dirent
移除即可。
22.Reading Directories
目录的实际结构取决于文件系统的实现,早期的版本中目录这个结构体由两个成员组成,一个是目录名,一个是inode号如下:
ino_t d_ino; /* i-node number */
char d_name[]; /* null-terminated filename */
大部分的文件系统实现都不允许程序直接使用read
来读取目录,所以提供了如下函数来供用户使用:
#include <dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
struct dirent *readdir(DIR *dp);
void rewinddir(DIR *dp);
int closedir(DIR *dp);
long telldir(DIR *dp);
void seekdir(DIR *dp, long loc);
fdopendir
和opendir
返回的DIR是一个对用不可见的结构,主要用于保存所读取的目录的信息,并且接下来中被readdir等函数所使用。注意,一个目录下的entry的顺序一般来说都不是按照字母顺序的。
23. chdir, fchdir, and getcwd Functions
每一个进程都有自己的工作路径,该路径是所有的相对路径的起始点。当用户登录的时候,此时的路径是/etc/password
中的第六项。我们可以通过chdir和fchdir来修改进程的工作路径,函数签名如下:
#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
如果在shell执行一个程序修改了程序的工作路径,这并不会影响到shell本身的工作路径。实际上,从shell的工作角度来说,执行一个命令就是用fork来创建新进程,然后再去执行命令的内容,所以这是相互隔离的。内核必须知道每一个进程的工作路径(不然的话,内核又是怎么知道相对路径是从哪里开始的呢?),但是内核并不是持有一个工作路径的全名,而是一个指向工作路径所属的inode指针。这个指针一般都会放在进程这个结构体中。在linux中,/proc/self/cwd
指向的就是当前进程的工作路径。可以通过getcwd函数来获得当前进程的工作路径,如下:
#include <unistd.h>
char *getcwd(char *buf, size_t size);
会将工作路径放入到buf当中,因此buf要足够大。对于shell而言,cd就是当前进程的工作路径的,所以cd的并不是通过fork来执行的。而且工作路径是可以为symbolic link的。