第三章说明了关于文件I/O的基本函数,主要是针对普通regular类型文件。本章描述文件的属性,除了regular文件还有其他类型的文件。
函数stat、fstat、fstatat和lstat
stat系列函数用于返回文件的属性信息,比如文件类型、大小、所有者、访问修改时间等。其头文件及函数原型如下:
#include <sys/stat.h> int stat (const char *restrict file, struct stat *restrict buf); int fstat (int fd, struct stat *buf); int lstat (const char *restrict file, struct stat *restrict buf); int fstatat (int fd, const char *restrict file, struct stat *restrict buf, int flag);
上面的函数,成功时返回0,失败返回-1。其中的stat结构体各个实现可能存在差异,但它们都至少具备下列的信息:
struct stat { mode_t st_mode; //文件类型 ino_t st_ino; //指向数据块的节点的编号 dev_t st_dev; dev_t st_rdev; nlink_t st_nlink; //硬链接数 uid_t st_uid; //文件用户ID gid_t st_gid; //文件用户组ID off_t st_size; struct timespec st_atim; //文件数据访问时间 struct timespec st_mtim; //文件数据修改时间 struct timespec st_ctim; //文件属性修改时间 blksize_t st_blksize; blkcnt_t st_blocks; };
文件类型
UNIX系统中的文件大多数是普通文件和目录,但也存在其他类型的文件,其分类如下:
- 普通文件(regular file):包含数据的常规文件,数据可以是文本类型的,也可以是二进制的。
- 目录文件(directory file):如字面所言,它是一个目录,保存目录相关的信息。
- 块设备文件(block special file):通常是硬件设备,被抽象成文件,比如硬盘。带有缓冲功能。
- 字符设备文件(regular file):通常是硬件设备,被抽象成文件,比如键盘。不支持缓冲。
- FIFO:进程间通信的一种文件。
- 套接字文件(regular file):网络间通信的一种文件。
- 符号连接(regular file):类似Windows系统的快捷方式。
以上文件类型的信息存储在前面说明的stat结构体中st_mode成员中的。st_mode成员的读取是利用系统提供的宏函数进行的。
设置用户ID和设置组ID
在UNIX系统中,一个进程至少有3大类6个以上的ID信息,这些信息主要是用来判定进程身份和权限的。当一个用户登入到UNIX系统之后,可以通过当前的交互shell来执行他自己的程序,此时执行起来的进程必须带有身份信息以便判断进程是否有权限访问文件。
进程首先得是一个可执行文件,该可执行文件被加载到内存执行起来之后才是一个进程。因为文件一定是属于某个用户的,也即属于某个ID的,所以执行后的进程也就是属于这个ID,这个ID叫做实际用户ID。但实际中,UNIX并非直接用这个实际用户ID,而是复制一份这个实际用户ID作为有效用户ID。文件的访问权限等判断是根据这个复制的有效用户ID来进行的。
在UNIX中,提供了一个额外的功能,那就是Set UID和Set GID,通过使用该功能可以将用户的身份切换到文件的所有者身份。举例来说,假设一个用户当前以admin账号的身份登录到系统上,那么该用户执行程序时,程序的实际身份和有效身份是admin,如果一个程序文件设置了Set UID权限为root,那么该程序被加载到内存中成为进程后,进程的有效身份不是admin,而是root,但实际身份保持admin不变。如下图所示,利用chmod命令手动对一个可执行文件增加Set UID权限,然后使用普通用户执行,其有效身份在执行过程中临时切换到了root:
文件访问权限
stat结构体中st_mode成员除了包含文件类型还包含了文件的访问权限,所有的文件都有权限控制,而不仅仅是普通文件有。为了打开任意类型的一个文件,则需要对该文件所在的父级以及父级的父级等目录具有执行权限。删除一个文件不需要对该文件有任何权限,只需要对被删除文件的父级目录具有写和执行权限即可。
进程每次打开、创建或删除一个文件时,内核就对该文件进行访问权限测试,通常的步骤是:
- 先判断进程是否是超级用户,即ID是0,是则允许访问,否则执行第二步;
- 再判断进程的有效用户ID是否等于文件的所有者ID,如果是并且被访问文件设定了适当的读写权限,则允许访问;否则执行第三步;
- 然后判断进程有效组ID或者附加组ID是否等于文件的组ID,如果是并且被访问文件设定了适当的读写权限,则允许访问;否则执行第四步;
- 最后查看文件的其他用户是否有适当权限访问文件,有则允许,否则判断结束、访问失败。
新文件和目录的所有权
当进程创建一个文件时,该文件的权限是进程的有效用户ID。而文件的组ID则有两种可能,POSIX规定可以是任意下面两种之一:
1.进程的有效组ID
2.新文件所在父级目录的组ID
函数access和faccessat
在使用open函数打开一个文件时,内核会进行文件访问权限的判定测试。如前所述,对权限的比较是通过进程的有效用户ID来进行的,但有时该进程的有效身份可能因为程序文件被设置了Set UID导致有效身份并非用户的实际身份,而用户想要该进程用自己的实际身份去测试文件是否可访问,而不是用有效身份去测试,此时可以使用access和faccessat函数来测试。access和faccessat函数会使得内核使用进程的实际用户ID去进行访问权限的判定,其头文件及函数原型如下:
#include <unistd.h> int access (const char *name, int type); int faccessat (int fd, const char *file, int type, int flag);
上面的函数,成功时返回0,失败返回-1。
函数umask
函数umask用来辅助控制程序所创建的文件的权限。其头文件及函数原型如下:
#include <sys/stat.h>
mode_t umask (mode_t mask);
该函数没有出错返回,它返回之前的umask值。
函数chmod、fchmod和fchmodat
chmod、fchmod和fchmodat这三个函数用于更改一个文件的访问权限。其头文件及函数原型如下:
#include <sys/stat.h> int chmod (const char *file, mode_t mode); int fchmod (int fd, mode_t mode); int fchmodat (int fd, const char *file, mode_t mode, int flag);
粘着位
粘着位原本用来阻止常用程序在结束后彻底退出内存,设置粘着位可以在系统重启前将常用的程序在使用完毕后不退出,而是把程序正文代码部分保存在内存交换区,以便下次快速载入启动,现今该技术已被淘汰,因为有了虚拟存储系统和更快更大的内存以及快速的文件系统。现在的系统扩展了粘着位的使用范围,允许对目录而不是程序设置粘着位。如果对一个目录设置了粘着位,那么对该目录具有写权限的用户具备下列三个条件之一,才能删除或重命名该目录下的文件:
- 拥有这个欲操作的文件,也即是文件的所有者;
- 拥有这个目录,也即是目录的所有者;
- 是超级用户。
目录/tmp是典型的粘着位的使用例子,所有的用户都有权限对其进行读写和执行,这个目录是一个缓存目录,所有用户的临时文件都能存放在这里,它会在每次重启系统或者定时被清空。由于所有人的临时文件都可以放在这里,并且所有人都能读写执行这个目录,因此就有必要阻止一个用户删除了其他用户的文件,粘着位就派上用场了。/tmp目录设置了粘着位,那么除非满足上面三个条件之一,否则无法删除文件。
函数chown、fchown、fchownat和lchown
chown系列函数用来改变文件的所属用户和所属组。其头文件及函数原型如下:
#include <sys/stat.h> int chmod (const char *file, mode_t mode); int lchmod (const char *file, mode_t mode); int fchmod (int fd, mode_t mode); int fchmodat (int fd, const char *file, mode_t mode, int flag);
上面的函数,成功时返回0,失败返回-1。
文件长度
stat结构体中st_size成员的值表示以字节为单位的文件长度,该字段只对普通文件、目录文件和符号连接有意义,其他类型文件无意义。对于普通文件,如果其st_size值为0,则文件长度是0,读取该文件时,会立即返回eof文件结束标记。对于符号连接,文件长度是符号连接文件中保存的字符串的总长度。
文件中的空洞
在第三章中学习过关于文件偏移量的内容,当设置一个偏移量值大于文件的实际长度并在偏移量之后写入了数据,那么介于实际文件长度之后和偏移量之前的那段就形成了空洞,对于空洞,read函数读作0,。如果用copy命令复制一个带空洞的文件,那么复制后的目标文件的空洞会被真实填充为0。
文件截断
truncate函数用于将一个文件截断为指定长度。其头文件及函数原型如下:
#include <unistd.h> int truncate (const char *file, off_t length); int ftruncate (int fd, off_t length);
上面的函数,成功时返回0,失败返回-1。
函数link、linkat、linkat、unlink和remove
link系列函数用来添加或解除对一个文件或目录的连接。其头文件及函数原型如下:
#include <unistd.h> int link (const char *from, const char *to); int linkat (int fromfd, const char *from, int tofd, const char *to, int flags); int unlink (const char *name); int unlinkat (int fd, const char *name, int flag); #include <stdio.h> int remove (const char *filename);
上面的函数,成功时返回0,失败返回-1。
函数rename和renameat
函数rename和renameat用于对文件或者目录重命名。其头文件及函数原型如下:
#include <stdio.h> int rename (const char *old, const char *new); int renameat (int oldfd, const char *old, int newfd, const char *new);
上面的函数,成功时返回0,失败返回-1。
符号链接
符号连接相当于Windows系统上的快捷方式,它与硬链接不同,硬链接具有下面的两条限制:
- 硬链接必须和所链接文件处于同一文件系统。
- 只有超级用户才能创建执行目录的硬链接。
而符号链接则没有上述限制。
创建和读取符号链接
函数symblink和symblinkat函数用于创建一个符号链接。其头文件及函数原型如下:
#include <unistd.h> int symlink (const char *from, const char *to); int symlinkat (const char *from, int tofd, const char *to);
上面的函数,成功时返回0,失败返回-1。
函数readlink和readlinkat用于读取一个符号链接。其头文件及函数原型如下:
#include <unistd.h> ssize_t readlink (const char *restrict path, char *restrict buf, size_t len); ssize_t readlinkat (int fd, const char *restrict path, char *restrict buf, size_t len);
上面的函数,成功时返回读取的字节数,失败返回-1。
文件的时间
UNIX系统中会对每个文件维护3个时间类型:
- st_atim:文件数据部分的访问时间
- st_mtim:文件数据部分的修改时间
- st_ctim:文件属性部分的修改时间
对于文件属性来说,其没有访问时间。
函数futimens、utimensat和utimes
文件的访问和修改时间可以利用这几个函数来进行更改。其头文件及函数原型如下:
#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 flags);
从这两个函数的名字可以看出,它们的精确度是ns纳秒级别的。成功时返回0,失败返回-1。
#include <sys/time.h> int utimes (const char *file, const struct timeval tvp[2]);
utimes函数和前两个函数不同,它是通过路径名来操作文件。成功时返回0,失败返回-1。
函数mkdir、mkdirat和rmdir
mkdir和mkdirat函数用来创建目录,rmdir函数用来删除空目录。其头文件及函数原型如下:
#include <sys/stat.h> int mkdir (const char *path, mode_t mode); int mkdirat (int fd, const char *path, mode_t mode); #include <unistd.h> int rmdir (const char *path);
上面的函数,成功时返回0,失败返回-1。
读目录
对于一个目录具有访问权限的用户都能读取该目录,只有内核才能写目录。一个目录的写和执行权限决定了能否在该目录中建立或者删除文件,但不表示这是写目录本身。关于目录的操作有一系列总计7个函数。其头文件及函数原型如下:
#include <dirent.h> DIR *opendir (const char *name); //成功返回指针,失败返回NULL DIR *fdopendir (int fd); //成功返回指针,失败返回NULL struct dirent *readdir (DIR *dirp); //成功返回指针,失败返回NULL void rewinddir (DIR *dirp); int closedir (DIR *dirp); //成功时返回0,失败返回-1 long int telldir (DIR *dirp); //成功时返回与dp关联的目录中的当前位置,失败返回-1 void seekdir (DIR *dirp, long int pos);
函数chdir、fchdir和getcwd
每个进程都要一个当前工作目录,此目录是搜索所有相对路径的起点。每个登入到操作系统的用户都有一个默认工作目录,该目录在配置文件/etc/passwd中每个用户相关信息的第6个字段中指明。进程可以使用chdir和fchdir来更改当前工作目录。其头文件及函数原型如下:
#include <unistd.h> int chdir (const char *path); int fchdir (int fd);
成功时返回0,失败返回-1。
有时进程也需要获取当前的工作路径,可以通过getcwd函数来获得。其头文件及函数原型如下:
#include <unistd.h> char *getcwd (char *buf, size_t size);
成功是返回buf,失败返回NULL。