第四章 文件和目录
Table of Contents
- 4. 第四章 文件和目录
- 4.1. stat、fstat和lstat函数
- 4.2. 文件类型
- 4.3. 设置用户ID和设置组ID
- 4.4. 文件访问权限
- 4.5. 新文件和目录的所有权
- 4.6. access函数
- 4.7. umask函数
- 4.8. chmod和fchmod函数
- 4.9. 粘住位
- 4.10. chown、fchown和lchown函数
- 4.11. 文件长度
- 4.12. 文件截短
- 4.13. 文件系统
- 4.14. link、unlink、remove和rename函数
- 4.15. 符号链接
- 4.16. symlink和readlink函数
- 4.17. 文件的时间
- 4.18. utime函数
- 4.19. mkdir和rmdir函数
- 4.20. 读目录
- 4.21. chdir、fchdir和getcwd函数
- 4.22. 设备特殊文件
第四章 文件和目录
stat、fstat和lstat函数
#include <sys/stat.h>
int stat(const char *restrict pathname ,struct stat *restrict buf);
int fstat(int filedes, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
restrict是C99引入,用来告诉编译器,指针是唯一操作对象的方式。
如果pathname是符号链接,lstat返回的符号链接本身的有关信息,stat返回的是符号链接引用文件的信息。
struct stat {
mode<sub>t</sub> st<sub>mode</sub>;/\* file type & mode **/
ino<sub>t</sub> st<sub>ino</sub>; /** i-node number **/
dev<sub>t</sub> st<sub>dev</sub>; /** device number (file system) **/
dev<sub>t</sub> st<sub>rdev</sub>; /** device number for special files\*/
nlink<sub>t</sub> st<sub>nlink</sub>; *\* number of links \**
uid<sub>t</sub> st<sub>uid</sub>; *\* user ID of owner \**
gid<sub>t</sub> st<sub>gid</sub>; *\* group ID of owner \**
off<sub>t</sub> st<sub>size</sub>; *\* size in bytes, for regular files \**
time<sub>t</sub> st<sub>atime</sub>;/\* time of last access\*/
time<sub>t</sub> st<sub>mtime</sub>;/\* time of last modification **/
time<sub>t</sub> st<sub>ctime</sub>; /** time of last file status change **/
blksize<sub>t</sub> st<sub>blksize</sub>; /** best I/O block size **/
blkcnt<sub>t</sub> st<sub>blocks</sub>; /** number of disk blocks allocated \*/
}
文件类型
文件类型有:
1. 普通文件
2. 目录文件
3. 块特殊文件。这种文件类型提供对设备带缓冲的访问,每次访问以固定长度为单位进行。
4. 字符特殊文件。不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。
5. FIFO。用于进程间通信,也叫命名管道。
6. 套接字,用于进程间网络通信,也可用于本机上进程间非网络通信。
7. 符号链接
设置用户ID和设置组ID
与每个进程相关的用户ID和组ID
项目 | 说明 |
---|---|
实际用户ID | 我们实际上是谁 |
实际组ID | |
———- | ————————- |
有效用户ID | 用于文件访问权限检查 |
有效组ID | |
附加组ID | |
———- | ——————————– |
保存的设置用户ID | 由exec函数保存 |
保存的设置组ID |
一般来说,有效用户ID就是实际用户ID,有效组ID就是实际组ID。但在文件模式字(stmode)中有两个特殊标志。
1. 设置用户ID, 当设置时,有效用户ID设置为文件所有者用户ID
2. 设置组ID,当设置时,有效组ID设置为文件所有者组ID
这两个设置过程是由exec操作的。
文件访问权限
目录的执行权限位被称为搜索位。
对于目录的读权限和执行权限意义是不同的。读权限允许我们读目录,获得在该目录中所有文件名的列表。当一个目录是我们要访问文件的路径名的一个组成部分时,
对该目录的执行权限使我们可以通过该目录(也就是搜索该目录,寻找一个特定的文件名)
内核进行的文件访问权限测试:
1. 若进程的有效用户ID是0,则允许访问。
2. 若进程的有效用户ID等于文件所有者ID.那么,若所有者有权限,则允许。
3. 若进程的有效组ID或进程有附加组ID之一等于文件的组ID,则依赖于组ID的权限位。
4. 若其它用户适当的访问权限位被设置,则允许访问。
说明:2,3,4按顺序检查时,如进程拥有此文件,则只检查第2步,3,4步不检查。其它情况类似。
新文件和目录的所有权
新文件的用户ID设置为进程的有效用户ID。
关于组ID,有如下设定:
1. 新文件的组ID可以是进程的有效组ID。
2. 新文件的组ID可以是它所在目录的组ID。
不同系统选择不一样,mac是第2个。
access函数
access函数是按实际用户ID和实际组ID进行访问权限测试的。
#include
int access(const char *pathname, int mode);
其中,mode是按下表按位或。
mode | 说明 |
---|---|
ROK | 测试读权限 |
WOK | 测试写权限 |
XOK | 测试执行权限 |
FOK | 测试文件是否存在 |
umask函数
umask函数为进程设置文件模式创建屏蔽字,并返回以前的值。(少数几个没有出错返回函数中的一个)
#include <sys/stat.h>
modet umask(modet cmask);
对于任何在文件模式创建屏蔽字中为1的位,在文件mode中的相应位则一定被关闭。
在登录时,会由shell启动文件设置一次umask,然后就不变了。
在编写创建新文件的程序时,如果要确保指定的访问权限位已激活,那么必须在进程运行时修改umask值。
更改进程的文件模式创建屏蔽字并不影响父进程。
chmod和fchmod函数
#include <sys/stat.h>
int chmod(const char *pathname, modet mode);
int fchmod(int filedes, modet mode);
为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID, 或者该进程必须有超级用户权限。
chmod函数更新的只是i节点最近一次被更改的时间。
设置用户ID和设置组ID由于安全原因。chmod有特殊处理, 在下列两个情况下会自动清除两个权限位。
1. 在Solaris等系统上,如果设置普通文件的粘住位(SISVTX),而且又没有超级权限,那么mode中的粘住位将自动被关闭。
2. 新创建文件的组ID可能不是调用进程所属的组(可能是父目录的组ID)。
特别地,如果新文件的组ID不等于进程的有效组ID或者进程附加组ID中的一个,以及用户没有超级用户权限,那么设置组ID位将会自动被关闭。
粘住位
历史技术,在UNIX还没有分页技术之前使用的。当时是用于可执行文件。
在一般UNIX文件系统中,文件的各数据块很可能是随机存放的。为了加速程序的载入,会在第一次执行后,把程序的正文部分存放到交换区,文件在交换区是连续的,这就是称为粘住位的原因,
也叫保存正文位(SISVTX)。
现在由于配置有虚拟存储系统以及快速文件系统,所以不再需要这种技术。
现今的系统扩展了粘住位的使用范围, 针对目录。如果对一个目录设置了粘住位,则只有对该目录有写权限的用户在满足下列条件之一的情况下,才能删除或更名该目录下的文件:
1. 拥有该文件
2. 拥有该目录
3. 是超级用户
可以共享一个目录,每个人都能修改自己的文件,但只能看别人的文件。
chown、fchown和lchown函数
#include
int chown(const char *pathname, uidt owner, gidt group);
int fchown(int filedes, uidt owner, gidt group);
int lchown(const char *pathname, uidt owner, gidt group);
在符号链接的情况下,lchown更改符号链接本身的所有者,而不是该符号链接所指向的文件。
如若两个参数owner或group中的任意一个是-1, 则对应的ID不变。
基于BSD的系统一直规定只有超级用户才能更改一个文件的所有者。这样做的原因是防止用户改变其文件的所有者从而摆脱磁盘空间限额对他们的限制。
系统V则允许任一用户更改他们所拥有的文件的所有者。
若POSIXCHOWNRESTRICTED对指定的文件起作用,则
1. 只有超级用户进程能更改该文件的用户ID。
2. 若满足下列条件,一个非超级用户进程就可以更改该文件的组ID:
1. 进程拥有此文件(其有效用户ID等于该文件的用户ID)。
2. 参数owner等于-1或文件的用户ID,并且参数group等于进程的有效组ID或进程的附加组ID之一。
如果这些函数由非超级用户进程调用,则在成功返回时,该文件的设置用户ID位和设置组ID位都会被清除。
文件长度
对于目录,文件长度通常是一个数(例如16或512)的倍数。
对于符号链接,文件长度是文件名中的实际字节数。
文件中的空洞
ls 看的是文件长度,du看的是文件所使用的磁盘空间总量。
正常的I/O操作读取整个文件长度,会把空洞读成0,如果使用实用程序(如cat)复制这种文件,那么所有空洞会被填满,填写为0。从而实际占用磁盘空间。
大文件,du报告的长度比ls的长,因为文件系统使用了若干块以存放指向实际数据块的各个指针。
文件截短
#include
int truncate(const char *pathname, offt length);
int ftruncate(int filedes, offt length);
这两个函数将把现有的文件长度截短为length字节。
如果文件以前的长度>length,则length以后的数据不能再访问,
如果以前的长度短于length,则其效果与系统有关。遵循XSI的系统会增加文件长度,产生空洞。
文件系统
文件系统分类的多种实现:
1. UFS, UNIX file system, 传统的基于BSD的UNIX文件系统。
2. PCFS, 读、写DOS格式化软盘的文件系统。
3. HSFS, 读CD的文件系统。
下面说的是UFS。
1个磁盘分成一个或多个分区,每个分区可以包含一个文件系统。
文件系统由自举块、超级块和若干个柱面组 组成。
每个柱面由 超级块副本、配置信息、i节点图、块位图、i节点和数据块组成。
i节点是固定长度的记录项,它包含有关文件的大部分信息:
文件类型、文件访问权限位、文件长度和指向该文件所占用的数据块的指针等等。
只有两项数据存放在目录项中:文件名和i节点编号。
每个文件系统各自对它们的i结点进行编号,因此目录项中的i节点编号数指向同一文件系统中的相应i节点,不能使一个目录项指向另一个文件系统的i节点。
link、unlink、remove和rename函数
#include
int link(const char *existingpath, const char *newpath);
创建新目录项newpath, 它引用现有的文件existingpath。
创建新目录项以及增加链接计数应当是个原子操作。
很多文件系统不允许对于目录的硬链接(可能形成循环)
删除目录项,可以调用unlink函数。
#include
int unlink(const char *pathname);
只有当链接计数达到0时,该文件的内容才可以被删除。另一个条件也会阻止删除文件的内容–只要有进程打开了该文件,其内容也不能删除。
关闭一个文件时,内核首先检查打开该文件的进程数。如果该数达到0,然后内核检查其链接数,如果这个数也是0,那么就删除该文件的内容。
上面的特性经常用来确保即使是在该程序崩溃时,它所创建的临时文件也不会遗留下来。
进程用open或creat创建一个文件,然后立即调用unlink。
remove函数解除对一个文件或目录的链接。对于文件,remove = unlink, 对于目录, remove = rmdir
#include
int remove(const char *pathname);
文件或目录用rename函数更名。
#include
int rename(const char *oldname, const char *newname);
- 如果oldname指的是一个文件而不是目录,那么为该文件/符号链接更名。
- 如若oldname指的是一个目录,则为该目录更名。
- 如若oldname或newname引用符号链接,则处理的是符号链接本身,而不是它所引用的文件。
- 作为一个特例,如果oldname=newname,函数直接返回。
如若newname已存在,则要求进程对其有写权限。另外,调用进程需要对包含oldname以及包含newname的目录具有写和执行权限。
符号链接
符号链接是指向一个文件的间接指针,它与硬链接不同,硬链接直接指向文件的i结点。引入符号链接的原因是为了避免硬链接的一些限制:
1. 硬链接通常要求链接和文件位于同一文件系统
2. 只有超级用户才能创建指向目录的硬链接
各个函数对符号链接的处理
函数 | 不跟随符号链接 | 跟随符号链接 |
---|---|---|
access | \* | |
chdir | \* | |
chmod | \* | |
chown | \* | |
creat | \* | |
lchown | \* | |
link | \* | |
lstat | \* | |
open | \* | |
opendir | \* | |
pathconf | \* | |
readlink | \* | |
remove | \* | |
rename | \* | |
stat | \* | |
truncate | \* | |
unlink | \* |
有个特例,同时用OCREATE和OEXCL两者调用open函数。在某些情况下,若路径名引用符号链接,open将出错返回,并将errno设置为EEXIST。这种处理方式的意图是堵塞一个安全性漏洞,
使具有特权的进程不会被诱骗对不适当的文件进行写操作。
symlink和readlink函数
symlink创建符号链接
#include
int symlink(const char *actualpath, const chat *sympath);
因为open函数会跟随符号链接,所以需要一种方法打开链接本身,并读该链接中的名字。readlink提供此功能。
#include
ssizet readlink(const char * restrict pathname, char *restrict buf, sizet bufsize);
此函数组成了open、read、close。成功返回读入buf的字节数。。
**需要注意的是buf不以null字符终止**。
文件的时间
与每个文件相关的三个时间值
字段 | 说明 | 例子 | ls选项 |
---|---|---|---|
statime | 文件数据的最后访问时间 | read | -u |
stmtime | 文件数据的最后修改时间 | write | 默认 |
stctime | i节点状态的最后修改时间 | chmod、chown | -c |
注意,系统并不保存对一个i节点的最后一次访问时间,所以access和stat函数并不更改这三个时间中的任一个。
utime函数
一个文件的访问和修改时间可用utime函数更改。
#include
int utime(const char *pathname, const struct utimbuf *times);
数据结构如下:
struct utimbuf {
timet actime;/* access time /
timet modtime;/ modification time */
}
此函数的操作以及执行它所要求的特权取决于times参数是否是NULL。
1. 如果times是一个空指针,则访问时间和修改时间两者都设置为当前时间。为了执行此操作必须满足下列两条件之一:进程的有效用户ID必须等于该文件的所有者ID;或者进程对该文件有写权限。
2. 如果times是非空指针,则访问时间和修改时间被设置为times所指向结构中的值。此时,进程的有效用户ID必须等于该文件的所有者ID,或者进程必须是一个超级用户进程。
mkdir和rmdir函数
#include <sys/stat.h>
int mkdir(const char *pathname, modet mode);
此函数创建一个新的空目录。其中,.和..目录项是自动创建的。所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。
对于目录通常至少要设置1个执行权限位,以允许访问该目录中的文件名.
用rmdir函数可以删除一个空目录。空目录是指只包含.和..这两项的目录。
#include
int rmdir(const char *pathname);
如果调用此函数使目录的链接计数成为0,并且也没有其他进程打开此目录,则释放由此目录占用的空间。
如果在链接计数达到0时,有一个或几个进程打开了此目录,则在此函数返回前删除最后一个链接及.和..项。
另外,在此目录中不能再创建新文件。但是在最后一个进程关闭它之前并不释放此目录。
读目录
为了防止文件系统产生混乱,只有内核才能写目录。一个目录的写和执行权限位,决定了在该目录下能否创建新文件和删除文件,它们并不表示能否写目录本身。
目录的实际格式依赖UNIX系统,特别是其文件系统的具体设计和实现。
很多实现阻止应用程序使用read函数读取目录的内容,从而进一步将应用程序与目录格式中与实现相关的细节隔离开。
#include
DIR *opendir(const char *pathname);
struct dirent *readdir(DIR *dp);
void rewinddir(DIR *dp);
int closedir(DIR *dp);
long telldir(DIR *dp);
void seekdir(DIR *dp, long loc);
最基础的定义:
struct dirent {
inot dino;
char dname[NAMEMAX + 1];
}
chdir、fchdir和getcwd函数
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。
进程通过chdir或fchdir函数可以更改当前工作目录。
#include
int chdir(const char *pathname);
int lchdir(int filedes);
内核为每个进程只保存指向该目录v节点的指针等目录本身的信息,并不保存该目录的完整路径名。
通过getcwd可以取得完整路径名。
#include
char *getcwd(char *buf, sizet size);
设备特殊文件
stdev 与 strdev容易引起混淆。
1. 每个文件系统所在的存储设备都由其主、次设备号表示。主设备号标识设备驱动程序,有时编码为与其通信的外设板;次设备号标识特定的子设备。次设备号标识分区?
2. 可以用宏major和minor来访问主、次设备号。
3. 系统中与每个文件名关联的stdev值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的i节点。
4. 只有字符特殊文件和块特殊文件才有strdev值,此值包含实际设备的设备号。