UNIX系统高级编程——第四章-文件和目录-总结
文件系统:
以UNIX系统V文件系统为例:
磁盘分为区,每个分区都有自己的文件系统:
i节点是固定长度的记录项,包含了文件的相关信息。目录项包含文件名和i节点号。stat结构中除文件名和i节点编号之外的数据都包含在i节点中
在进行文件索引的时候会首先找到目录项,根据目录项的i节点号找到i节点,i节点中包含了若干数据块的引用,这些数据块就是文件内容。目录项只是对i节点的引用
多个目录项可以引用相同的i节点,i节点将记录引用它的目录项的个数,只有在没有目录项引用它的时候才可以删除其中的数据。所以删除一个目录项并不意味着一定删除对应的数据。这种连接被称为硬链接。与之相对的是符号链接:i节点指向的实际文件内容是另一个文件的名字,i节点中的文件类型将指示这是否是个符号链接
目录项中的i节点编号指向的是同一文件系统中的i节点,不能使用另一个文件系统的i节点
文件重命名只是构造一个新的指向对应i节点的目录项并删除现有的目录项,而不会改变文件的实质内容
link/unlink/remove/rename函数:
link函数创建已有文件的连接:参数为现有文件路径和新文件路径,成功后新文件路径将引用和现有文件路径相同的文件内容,如果新文件路径已经存在就返回错误。创建新目录项和增加连接计数应是原子操作
只有超级用户才可以创建指向目录的连接,因为这可能会导致文件系统中形成循环,大多数文件系统公有函数无法处理循环
unlink删除现有目录项,内核先后检查打开该文件的进程数和i节点的连接计数,如果都是0就删除文件内容。如果参数是符号链接,那么删除的是符号链接而不涉及其指向的文件内容
remove对于文件和unlink相同,对于目录和rmdir相同
将oldname指向的文件更名为newname
- oldname是文件而不是目录。如果newname已存在,则不能为目录,删除newname的目录项,并将oldname更名为newname
- oldname是目录。如果newname已存在,则必须为空目录(目录下只有"."和".."),删除newname的目录项。将oldname更名为newname。oldname不能是newname的路径前缀
oldname和newname指向同一文件。不做处理直接返回成功
符号连接:
符号连接的目的是避免硬连接的某些限制:
- 硬链接通常要求和文件位于同一个文件系统
- 只有超级用户才能创建目录的硬连接
需要注意文件处理函数是否能够处理符号连接
在文件系统中形成循环的符号连接可以很容易通过unlink函数删除,因为unlink不跟随符号连接。但是形成循环的硬连接很难删除
符号连接存在,但是其指向的文件可能不存在,所以在使用open函数打开一个存在的符号连接可能会因为目标文件不存在而产生错误
symlink和readlink函数:
symlink函数创建一个符号连接,不要求被指向的文件存在或者存在于同一个文件系统
因为open函数会跟随符号连接,所以需要使用readlink函数读取符号连接相关的信息
mkdir和rmdir函数:
mkdir创建目录,rmdir删除目录
创建的新的空目录,"."和".."自动创建,文件存取权限按照进程在创建之前设置的文件方式屏蔽字设置
rmdir可以删除空目录。如果调用使得目录的连接计数为0并且没有其他进程打开目录,就释放目录占用的空间。如果有其他的进程打开此目录,就在函数返回之前删除最后的连接以及"."和"..",但是在最后一个进程释放它之前不会释放它占用的空间。此目录下将无法再创建文件
读目录:
对目录有存取权限的用户都可以读目录,只有内核可以写目录
目录的写和执行权限只是可以在该目录下创建或者删除文件
chdir/fchdir/getcwd函数:
进程使用chdir和fchdir函数更改当前工作目录
内核为进程维护当前工作目录的时候只维护其i节点编号及设备标识,所以没有完整的路径名。可以使用函数getcwd获取完整的路径
sync和fsync函数:
UNIX系统在内核实现了缓冲器,大多数磁盘I/O将先写到缓冲(即内存)里面,之后再写到磁盘中。当系统故障时可能会有数据没有写到磁盘中
sync函数将所有缓冲中修改过的块排入写磁盘队列,然后返回,不等待I/O结束。系统精灵进程将会每隔一段时间调用sync
fsync函数只对单个文件起作用,将文件内容更新到磁盘中,等待I/O结束再返回。O_SYNC与之不同的是每次调用write都会直接更新文件内容到磁盘
stat:
来自gcc version 7.3.0的sys/stat.h文件
struct stat { unsigned long st_dev; /* Device. */ unsigned long st_ino; /* File serial number. */ unsigned int st_mode; /* File mode. */ unsigned int st_nlink; /* Link count. */ unsigned int st_uid; /* User ID of the file's owner. */ unsigned int st_gid; /* Group ID of the file's group. */ unsigned long st_rdev; /* Device number, if device. */ unsigned long __pad1; long st_size; /* Size of file, in bytes. */ int st_blksize; /* Optimal block size for I/O. */ int __pad2; long st_blocks; /* Number 512-byte blocks allocated. */ long st_atime; /* Time of last access. */ unsigned long st_atime_nsec; long st_mtime; /* Time of last modification. */ unsigned long st_mtime_nsec; long st_ctime; /* Time of last status change. */ unsigned long st_ctime_nsec; unsigned int __unused4; unsigned int __unused5; };
st_dev/st_rdev
每个文件系统都有主、次设备号,存放于基本系统数据类型dev_t。在这里是unsigned long类型,这个unsigned long通过特定的方式解析为主设备号和次设备号。
系统中每个文件的st_dev包含的是文件系统的主次设备号。只有字特殊文件和块特殊文件才有st_rdev值,包含实际设备的设备号
使用major和minor函数从dev_t类型中存取主次设备号
st_ino
i节点号
st_mode
有三部分可以从此数字解析:存取权限、文件类型、设置-用户/组-ID位
文件类型:
- 普通文件:文件中包含某种形式的数据,内核不关心数据,数据解析由相关应用程序完成
- 目录文件:包含了其他文件的名字以及指向与这些文件有关信息的指针。任何有访问权限的进程都可以访问目录的内容,但是只有内核可以写目录文件
- 字符特殊文件:用于系统中某些类型的设备
- 快特殊文件:典型的用于磁盘设备。系统中所有设备是字特殊文件或者是块特殊文件
- FIFO:命名管道
- socket:用于进程间通信的文件
- 符号链接:此种文件指向另一个文件
文件类型可以通过在stat.h中定义的宏判断:
文件权限:
文件有9个主要的权限,下面是权限位的掩码(来自stat.h):
// 所有者(用户)权限位
#define S_IRWXU 00700 /* 用户读写执行 */ #define S_IRUSR 00400 /* 用户读 */ #define S_IWUSR 00200 /* 用户写 */ #define S_IXUSR 00100 /* 用户执行 */
// 组权限位 #define S_IRWXG 00070 /* 组读写执行 */ #define S_IRGRP 00040 /* 组读 */ #define S_IWGRP 00020 /* 组写 */ #define S_IXGRP 00010 /* 组执行 */
// 其他权限位 #define S_IRWXO 00007 /* 其它读写执行 */ #define S_IROTH 00004 /* 其它读 */ #define S_IWOTH 00002 /* 其它写 */ #define S_IXOTH 00001 /* 其它执行 */
分为三组,每组的第一个是其余三个掩码的按位或,所以实际上还是9个权限位。
chmod和fchmod函数用于改变文件的存取权限位,前者使用文件名作为参数,后者使用打开的文件作为参数
下列情况下这两个函数对于某个权限位将不会按照参数中的权限进行设置,而是将对应的权限位清空:
- 在没有超级用户权限的情况下,试图设置文件的粘住位。此时粘住位将被置0
- 在没有超级用户权限的情况下,试图设置文件的组ID为既不是创建进程的有效组ID也不是创建进程的添加组ID。此时设置-组ID位将被置0
权限位测试:
进程在打开、创建或删除文件的时候,将会按照以下步骤进行文件的权限位测试:
- 若进程的有效用户ID是超级用户,则允许任意的存取。否则进入下一步
- 若进程的有效用户ID是文件的所有者ID,则按照文件的三个所有者权限位。否则进入下一步
- 若进程的有效组ID或添加组ID是文件的组ID,将按照文件的三个组权限位。否则进入下一步
- 按照文件的三个其他权限位
设置-用户/组-ID位 :(两位,分别对应用户和组)
进程有以下ID:
- 实际用户/组ID:实际执行进程者
- 有效用户/组ID,添加组ID:用于文件存取权限测试
- 保存设置用户/组ID
通常情况下有效用户/组ID就是实际用户/组ID,但是可以通过设置文件st_mode中的设置-用户/组-ID位来使进程在打开该文件的时候有效用户/组ID为文件的所有者
两位通过S_ISUID和S_ISGID宏测试
粘住位:
早期UNIX系统中,交换区的文件是连续存放的,而其他的位置可能是随机存放的。所以可以使用粘住位将一个可执行程序文件的正文(机器指令部分)固定在交换区,下次执行的时候将能够更快的放入内存。但是较新的UNIX系统都具有虚拟内存和快速文件系统,所以不再需要这种技术
SVR4和4.3+BSD中粘住位主要针对目录,在目录设置了粘住位之后,对该目录具有写权限的用户在满足下列情况之一的情况下才可以删除或重命名目录下的文件:拥有文件/拥有目录/超级用户
umask函数:
用于设置和进程相关的文件方式创建屏蔽字。在进程创建文件的时候就会根据文件方式创建屏蔽字以及创建时指定的权限为文件设置读写权限。
文件方式创建屏蔽字为1的位,无论创建时指定的权限位为0/1都会将对应的权限位设置为0
st_uid/st_gid
新文件的用户/组ID:
在创建新文件、目录的时候,新文件的用户ID为创建进程的有效用户ID
POSIX规定,组ID可以通过下面两种方式之一确定:
- 进程的有效组ID
- 所在目录的组ID
SVR4系统中,如果文件的设置组ID位被设置,那么新文件的组ID为所在目录的组ID,否则为进程的有效组ID
4.3+BSD系统中始终使用所在目录的组ID
access函数:
验证实际用户对文件的存取权限
以文件路径的方式指定需检查的文件,第二个参数指定需要检查的项
- R_OK 测试读许可权
- W_OK 测试写许可权
- X_OK 测试执行许可权
- F_OK 测试文件是否存在
chown/fchown/lchown函数:
用于更改文件的用户ID和组ID
符号链接:lchown更改符号链接本身的所有者,其他两个更改符号链接指向的文件的所有者
只有SVR4支持lchown,在非SVR4系统中,如果chown的参数是符号链接就改变其本身的所有者,如果想要改变其指向的文件,就需要直接指定该文件
POSIX.1规定按照_POSIX_CHOWN_RESTRICTED的值设置更改文件所有者是否受限
若起作用,则:
- 只有超级用户才能改变文件的用户ID
- 在下列情况下非超级用户可以改变文件的组ID
- 进程的有效用户ID为文件的用户ID
- 参数owner等于文件的用户ID,参数group等于进程的有效组ID或者进程的添加组ID
函数在被非超级用户调用并成功返回后文件的设置-用户ID和设置-组ID被清除
st_size
指明文件长度,只对普通文件、符号连接、目录文件起作用。SVR4支持对管道表示管道中可读取的字节数
符号链接的长度表示其指向文件名的字节数,因有长度指定,所以不以null结尾
普通文件的长度可以为0,但是目录和符号连接不会为0。因为目录至少包含"."和".."两项,符号连接的文件内容是文件路径,文件路径至少有一个字符
SVR4和4.3+BSD支持使用st_blksize和st_blocks表示块大小和块数
文件空洞:偏移量超过文件结尾进行写数据,文件结尾和新写的数据之间就是文件空洞。
文件空洞不会用实际的空间存储,所以st_size要比st_blksize*block得到的大(即便除去额外的维护所需空间)。使用read()函数读取得到的是0。
但是如果使用如cat(1)进行文件复制,会将空洞复制为实际的字节0而占用实际的存储空间
文件截短:
使用truncate和ftruncate将文件截短至指定的长度
对于文件长度大于截断长度的,截断长度之后的内容无法读取
对于文件长度小于截断长度的,根据实际实现的系统而定
st_atime/st_mtime/st_ctime
每个文件都有三个时间:
- st_atime:文件数据的最后存取时间
- st_mtime:文件数据的最后修改时间
- st_ctime:i节点状态的最后修改时间
utime函数用于更改一个文件的存取和修改时间
参数需传递一个包含两个时间的结构体
- 结构体为null:将存取时间和修改时间设置为当前时间。需要满足进程的有效用户ID为文件的所有者ID或者进程对该文件有写权限
- 结构体不为null:将存取时间和修改时间设置为指定的值。需要满足进程的有效用户ID为文件的所有者ID或者进程是超级用户进程
不能修改修改状态时间(st_ctime)