04.文件和目录
1、文件类型
a、普通文件:这种文件包含了某种形式的数据,至于这种数据是文本还是二进制数据对于UNIX内核而言并无区别,但对于标准I/O流而言,二进制和文本文件是有区别的;
b、目录文件:这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针(见dirent结构体成员)。对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件(内核提供给用户在目录中新建文件的函数,而该函数内部处理由内核完成,用户不能像写文件内容一样操作目录文件的内容);
c、块特殊文件:这种文件类型提供对设备(例如磁盘)带缓冲的访问,每次访问以固定长度为单位进行;
d、字符特殊文件:这种文件类型提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊设备,要么是块特殊文件;
e、FIFO:这种类型文件用于进程间通信,有时也称其为命名管道;
f、套接字:这种文件类型用于进程间的网络通信,也可用于在一台宿主机上进程之间的网络通信;
g、符号链接:这种文件类型指向另一个文件。
文件类型信息包含在stat结构的st_mode成员中:
宏 文件类型
S_ISREG(stat.st_mode) 普通文件
S_ISDIR(stat.st_mode) 目录文件
S_ISCHR(stat.st_mode) 字符特殊文件
S_ISBLK(stat.st_mode) 块特殊文件
S_ISFIFO(stat.st_mode) FIFO文件
S_ISLNK(stat.st_mode) 符号链接
S_ISSOCK(stat.st_mode) 套接字
2、设置用户ID和设置组ID
设置用户ID和设置组ID(进程相关)
与一个进程相关联的ID有6个或更多:
---------------------------------------------------------------------------
实际用户ID 我们究竟是谁
实际组ID
---------------------------------------------------------------------------
有效用户ID 用于文件访问权限检查
有效组ID
附加组ID
---------------------------------------------------------------------------
保存的设置用户ID 由exec函数保存
保存的设置组ID
---------------------------------------------------------------------------
a、实际用户ID和实际组ID标识我们究竟是谁。这两个字段在登录时取自口令文件中的登录项;
b、有效用户ID,有效组ID和附加组ID决定了我们的文件访问权限;
c、保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有效用户ID和有效组ID的副本。
通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID。
上面的ID都与进程相关联。
下面的是文件相关ID
每个文件都有一个所有者和组所有者,所有者由stat结构中的s_uid成员表示,组所有者则由st_gid表示。
给执行一个程序文件时,进程的有效用户ID通常就是实际用户ID,有效组ID通常是实际组ID,但是可以在文件模式字(st_mode)中设置一个特殊标志,含义是“当执行此文件时,将进程的有效用户ID设置为文件所有者的用户ID(st_uid)”。与此类似,在文件模式字中设置另一位,它使得将执行此文件的进程的有效组ID设置为文件的组所有者ID(st_gid)。在文件模式字中的这两位被称为设置用户ID位和设置组ID位(这两位记录在文件中)。
如,若文件所有者是超级用户,而且设置了该文件的设置用户ID位,然后当该程序由一个进程执行时,则该进程具有超级用户特权,不管此文件的进程的实际用户ID是什么,都进行这种处理。
文件的设置用户ID标志位以及设置组ID标志位都包含在st_mode值中,这两位分别由S_ISUID和S_ISGID常量表示。
S_ISGID位和S_ISUID位的判断:
通过文件模式字st_mode与S_ISGID或者S_ISUID进行“按位与”运算可以判断对应位的状态,即:
st_mode & S_ISGID:取得S_ISGID位的状态。 st_mode & S_ISUID:取得S_ISUID位的状态。
S_ISGID位和S_ISUID位的置位:
通过文件模式字st_mode与S_ISGID或者S_ISUID进行“按位或”运算可以置位对应位,即:
st_mode | S_ISGID:置位S_ISGID位。 st_mode | S_ISUID:置位S_ISUID位。
S_ISGID位和S_ISUID位的清除:
通过文件模式字st_mode与S_ISGID或者S_ISUID反码进行“按位与”运算可以清除对应位,即:
st_mode & (~S_ISGID):清除S_ISGID位。 st_mode & (~S_ISUID):清除S_ISUID位。
文件st_mode的定义值
The following flags are defined for the st_mode field:
S_IFMT bit mask for the file type bit fields
S_IFSOCK socket
S_IFLNK symbolic link
S_IFREG regular file
S_IFBLK block device
S_IFDIR directory
S_IFCHR character device
S_IFIFO FIFO
S_ISUID set UID bit
S_ISGID set-group-ID bit (see below)
S_ISVTX sticky bit (see below)
S_IRWXU mask for file owner permissions
S_IRUSR owner has read permission
S_IWUSR owner has write permission
S_IXUSR owner has execute permission
S_IRWXG mask for group permissions
S_IRGRP group has read permission
S_IWGRP group has write permission
S_IXGRP group has execute permission
S_IRWXO mask for permissions for others (not in group)
S_IROTH others have read permission
S_IWOTH others have write permission
S_IXOTH others have execute permission
3、文件访问权限
我们用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。
对目录的读权限和执行权限的意义是不同的。读权限允许我们读目录,获得在该目录中所有文件名的列表。当一个目录是我们要访问文件的路径名的一个组成部分时,对该目录的执行权限使我们可通过该目录(也就是搜索该目录,寻找一个特定的文件名)。
如果PATH环境变量指定了一个我们不具有执行权限的目录,那么shell绝不会在该目录下找到可执行文件。
为了在目录中创建/删除一个文件,必须对该目录具有写和执行权限。如果用6个exec函数中的任何一个执行某个文件,都必须对该文件具有执行权限,该文件还必须是一个普通文件。
进程每次打开、创建或者删除一个文件时,内核就进行文件访问权限测试(文件的所有者ID位,st_uid和st_gid,进程的有效用户ID和有效组ID):
a、若进程的有效用户ID是0(超级用户),则允许访问;
b、若进程的有效用户ID等于文件的所有者ID(也就是该进程拥有该文件),那么:若所有者适当的访问权限位被设置,则允许访问,否则拒绝访问。适当的访问权限位指的是,若进程为读而打开该文件,则用户读位为1;若进程为写而打开该文件,则用户写位应为1;若进程将执行该文件,则用户执行位应为1;
c、若进程的有效组ID或进程的附加组ID之一等于文件的组ID,那么:若组适当的访问权限位被设置,则允许访问,否则拒绝访问;
d、若其他用户适当的访问权限位被设置,则允许访问;否则拒绝访问。
按顺序执行这四步,注意,如若进程用于此文件(第2步),则按用户访问权限批准或拒绝该进程对文件的访问---不查看组访问权限。类似地,若进程并不用于该文件,但进程属于某个适当的组,则按组访问权限批准或拒绝该进程对该文件的访问---不查看其他用户的访问权限。
如前所述,当用open函数打开一个文件时,内核以进程的有效用户ID和有效组ID为基础进行其访问权限测试,有时,进程也希望按其实际用户ID和实际组ID来测试其访问能力。access函数是按实际用户ID和实际组ID进行访问权限测试的。
文件访问权限位小结
4、新文件和目录的所有权
新文件的用户ID设置为进程的有效用户ID。关于组ID,POSIX.1允许实现选择下列之一作为新文件的组ID:
(1)新文件的组ID可以是进程的有效组ID;
(2)新文件的组ID可以是它所在目录的组ID;
5、umask函数
#define RWRWRW (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) int main() { umask(0); if(creat("foo",RWRWRW) < 0) printf("create error for foo!\n"); umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if(creat("bar",RWRWRW) < 0) printf("create error for bar!\n"); exit(0); }
运行结果:
umask 0022 -rw------- 1 xxx xxx 0 2012-07-25 22:56 bar -rw-rw-rw- 1 xxx xxx 0 2012-07-25 22:56 foo umask 0022
更改进程的文件模式创建屏蔽字并不影响其父进程(常常是shell)的屏蔽字。
6、更改文件权限位
chmod和fchmod函数
为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限。
7、文件长度
stat结构成员st_size表示以字节为单位的文件长度。此字段只对普通文件、目录文件和符号链接有意义。
文件中的空洞
空洞是由所设置的偏移量超过文件尾端。
对于没有写过的字节位置(空洞),read函数读到的字节是0.由此可见,正常的I/O操作读整个文件长度。
如果使用使用程序(如cat)复制这种文件,那么所有这些空洞都会被填满,其中所有实际数据字节皆填写为0.复制文件所占用的磁盘块将大于原文件,原因是,文件系统使用了若干块以存放实际数据块的各个指针。
所以,文件长度不一定与所占磁盘空间大小相等。
8、文件系统
我们可以把一个磁盘分成一个或多个分区,每个分区可以包含一个文件系统。如下图:
i节点是固定长度的记录项,它包含有关文件的大部分信息。
下图为更仔细的一个柱面组(分区)的i节点和数据块部分:
a、硬链接:两个目录项指向同一个i节点。每个i节点中都有一个链接计数,其值是指向该i节点的目录项数。只有当链接计数减少至0时,才可删除该文件(也就是可以释放该文件占用的数据块)。这就是为什么“解除对一个文件的链接”操作并不总是意味着“释放该文件占用的磁盘块”的原因。这也是为什么删除一个目录项的函数被成为unlink而不是delete的原因。在stat结构中,链接计数包含在st_nlink成员中,其基本系统数据类型是nlink_t。
不管硬链接文件是目录还是文件,它跟源目录或文件指向同一个i节点,删除源目录或文件也只是减少对应i节点的链接计数而已,实际的磁盘块由于硬链接的存在而没有被删除,当该i节点的链接计数为0时,才可释放该目录或文件对应的磁盘块。
b、符号链接:对于这种链接,该文件的实际内容(在数据块中)包含了该符号链接所指向的文件的名字,如:
lrwxrwxrwx 1 xxx xxx 10 2012-07-26 21:57 test.c -> ./c/test.c
在记录该文件的目录项中,该文件名是6字符的字符串test.c,而在该文件中包含了10个数据字节./c/test.c。该i节点中的文件类型是S_IFLNK,于是系统知道这是一个符号链接。
也就是说符号链接文件除了该i节点中的文件类型是S_IFLNK,其他的跟普通文件一样。该文件里面保存的数据为源文件的路径名。如果源文件删除的话,那么对应该文件的符号链接文件也不能被使用了。源文件对应的i节点链接计数值不随符号链接个数而改变。符号链接文件对应的i节点编号与其指向的文件的i节点编号不相等的。
符号链接避开硬链接的限制:
硬链接通常要求链接的文件位于同一个文件系统中(统一的i节点编号,当前的i节点编号只在本分区中有效);
只有超级用户才能创建指向目录的硬链接。
c、i节点包含了大多数与文件相关的信息:文件类型、文件访问权限位、文件长度和指向该文件所占用的数据块的指针等。stat结构中的大多数信息都取自i节点。只有两项存放在目录中:文件名和i节点编号。i节点编号的数据类型是ino_t。
d、每个文件系统各自对它们的i节点编号,因此目录项中的i节点编号数指向同一个文件系统中的相应i节点,不能使一个目录项指向另一个文件系统的i节点。这就是问什么ln命令不能跨越文件系统的原因。
e、当在不更换文件系统情况下为一个文件更名时,该文件的实际内容并未移动,只需构造一个指向现有i节点的新目录项,并解除与旧目录项的链接。例如,为将文件/usr/lib/foo更名为/usr/foo,如果目录/usr/lib和/usr在同一个文件系统中,则文件foo的内容无需移动。这就是mv命令的通常操作方式。
f、新建目录
假如在xxx目录下新建testdir目录:
mkdir testdir
如下图所示:
假如xxx目录对应的i节点编号为1267,testdir目录对应的i节点编号为2549.
对于编号为2549的i节点,其类型字段表示它是一个目录,而链接计数为2.任何一个叶目录(不包含任何其他目录的目录)的链接计数总是2,数值2来自于命名该目录(即xxx/testdir)的目录项以及在该目录中(即testdir/.)的.项。
对于编号为1267的i节点,其类型字段表示它是一个目录,而其链接计数则大于或等于3.它大于或等于3的原因是,至少有三个目录项指向它:一个命名它的目录项(即/xxx),第二个是在该目录中的.项(即/xxx/.),第三个是在其子目录中testdir的..项(即/xxx/testdir/..)。注意,父目录中的每一个子目录都会使该父目录的链接计数增1.
g、如何定位文件
系统都是根据当前文件的文件路径名来定位到i节点。不管是GUI界面还是console界面,系统都保存了当前工作目录,如果编辑当前目录下的文件,那么系统如何定位到该文件的i节点呢(同一文件系统下)?
由于系统保存进程的当前工作目录(此目录是搜索所有相对路径名的起点),根据当前文件名得到该文件的绝对路径。下面是定位过程(假如定位/etc/crontab文件):
【根目录/】 ---> 【根目录/的i节点编号】 ---> 【根目录的数据块,因为根目录是目录,所以数据块保存了该子目录或文件对应的i节点编号和它们名称,在这里得到etc目录的i节点编号】 ---> 【/etc目录的i节点】 ---> 【/etc目录的数据块,因为/etc为目录,所以数据块保存了该子目录或文件的i节点编号和它们名称,在这里得到crontab文件的i节点编号】 ---> 【/etc/crontab文件的i节点】。
注意,文件或目录的名称保存在其父目录中的数据块中,而非i节点中。
9、文件的时间
----------------------------------------------------------------------------
字段 说明 例子
----------------------------------------------------------------------------
st_atime 文件数据的最后访问时间 read
----------------------------------------------------------------------------
st_mtime 文件数据的最后修改时间 write
----------------------------------------------------------------------------
st_ctime i节点状态的最后更改时间 chmod、chown
----------------------------------------------------------------------------
utime函数用来更改文件数据的最后访问时间(st_atime)和最后修改时间(st_mtime)而不更改i节点状态时间(st_ctime)。
10、mkdir
mkdir创建一个新的空目录,其中.和..目录项都是自动创建的。
常见的错误是指定与文件相同的mode(只指定读、写权限)。但是,对于目录通常至少要设置1个执行权限位,以允许访问该目录中的文件名。
11、chdir更改当前工作目录
因为当前工作目录是进程的一个属性,所以它只音响调用chdir的进程本身,而不影响其他进程,如:
int main() { if(chdir("/tmp") < 0) { printf("chdir failed!\n"); exit(1); } printf("chdir to /tmp succeed!\n"); exit(0); }
结果为:
$ pwd /home/zzc/program/c $ ./test chdir to /tmp succeed! $ pwd /home/zzc/program/c
从中可以看出,执行test程序的shell的当前工作目录并没有改变,其原因是shell创建了一个子进程,由该子进程具体执行test程序。由此可见,为了改变shell进程自己的工作目录,shell应当直接调用chdir函数,为此cd命令的执行程序直接包含在shell程序中。
内核保持有当前工作目录的信息,内核为每个进程只保存指向该目录v节点的指针等目录本身信息,并不保存该目录的完整路径名。getcwd函数用..目录项找到其上一级的目录,然后读其目录项,直到该目录项中的i节点编号与工作目录i节点编号相同,这样就找到了其对应的文件名。按照此方法,直到遇到根,这样就得到了当前工作目录完整的绝对路径名。
char *getcwd(char *buf, size_t size); char *getwd(char *buf);