4个stat函数:
stat函数返回名为参数pathname的文件有关的信息结构。fstat函数获得已经在描述符fd上打开的文件的信息。lstat函数的参数pathname如果是一个符号链接文件,它返回符号链接的有关信息,而非该符号链接引用的文件的信息。fstatat函数为一个相对于当前打开目录(由fd参数表示)的路径名返回文件统计信息,参数flag为AT_SYMLINK_NOFOLLOW时返回符号链接本身的信息,否则,默认情况下返回符号链接所指向的文件的信息,参数fd如果为AT_FDCWD且pathname是一个相对路径名,则该相对路径名是相对于当前目录的,如果pathname参数是一个绝对路径,则fd参数被忽略。
以上函数的buf参数是一个指向我们必须提供的结构的指针,该结构的基本形式如下,具体实现会有不同:
timespec结构定义了时间,至少包括以下字段:
在2008年版以前的标准中,时间字段都是time_t类型(以秒表示),timespec结构提供了更高精度的时间戳,为保持兼容性,旧版时间可改为st_atime.tv_sec。
基本系统数据类型:如mode_t,由系统定义,这样不必考虑程序在移植时要改变类型等细节。
命令ls -l
中使用了stat函数。
文件类型:
1.普通文件,包含了文本或二进制数据,这对内核而言无区别,对普通文件内容的解释由处理该文件的应用程序进行。唯一的例外是二进制可执行文件,内核为了执行该文件必须理解其格式,所有二进制可执行文件都遵循一种标准化格式,使内核能确定程序文本和数据的加载位置。
2.目录文件,包含了目录中文件的名字和指向与这些文件有关信息的指针。对一个目录文件有读权限的任一进程都能读目录的内容,但只有内核可以直接写目录文件。
3.块特殊文件,提供对设备带缓冲的访问,每次访问以固定长度为单位进行。
4.字符特殊文件,提供对设备不带缓冲的访问,每次访问长度可变。系统中的设备要么是3要么是4。
5.FIFO,用于进程间通信。
6.套接字,用于进程间的网络通信,也可用于一台主机上进程间的非网络通信。
7.符号链接,该类型文件指向另一个文件。
文件类型信息包含在stat结构的st_mode成员中 ,可用以下宏测试该成员:
POSIX.1允许实现将进程间通信(IPC)对象说明为文件,可用以下宏确定IPC对象类型,但其参数是指向stat结构的指针:
取命令行参数,对每个参数打印其文件类型:
#include <stdio.h>
#include <sys/stat.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
int i;
struct stat buf;
char *ptr;
for (i = 1; i < argc; ++i) {
printf("%s: ", argv[i]);
if (lstat(argv[i], &buf) < 0) {
printf("lstat error\n");
continue;
}
if (S_ISREG(buf.st_mode))
ptr = "regular";
else if (S_ISDIR(buf.st_mode))
ptr = "directory";
else if (S_ISCHR(buf.st_mode))
ptr = "charactor special";
else if (S_ISBLK(buf.st_mode))
ptr = "block special";
else if (S_ISFIFO(buf.st_mode))
ptr = "fifo";
else if (S_ISLNK(buf.st_mode))
ptr = "symbolic link";
else if (S_ISSOCK(buf.st_mode))
ptr = "socket";
else
ptr = "** unknown mode **";
printf("%s\n", ptr);
}
exit(0);
}
运行它:
早期UNIX版本不提供S_ISxxx宏,而是将st_mode与屏蔽字S_IFMT逻辑与,然后与名为S_IFxxx的常量相比较,大多系统在头文件sys/stat.h中定义了这些常量,且其中的S_ISDIR的宏定义为:
一个单用户Linux系统中各个文件类型的占比:
实际用户ID和实际组ID标识我们是谁,这两个字段取自口令文件中的登录项,通常在一个登录会话期间值不变,但root可改变它们。
有效用户ID、有效组ID和附属组ID决定了我们的文件访问权限。
保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有效组ID和有效用户ID的副本。
保存的ID功能在POSIX.1 2001版之前是可选的,应用可在编译时测试常量_POSIX_SAVED_IDS或运行时以参数_SC_SAVED_IDS调用sysconf判断实现是否支持该功能。
每个文件都有一个所有者和组所有者,所有者由stat结构中的st_uid指定,组所有者由st_gid指定。
执行一个程序文件时,进程的有效用户ID就是实际用户ID,组ID同理。但文件的st_mode中可设置一个特殊标志,含义为当执行此文件时,将进程的有效用户ID设置为文件的所有者的用户ID,组ID同理,这两个标志位被称为设置用户ID位和设置组ID位。
passwd命令是一个打开了设置用户ID标志位的程序,且程序所有者为root,因为该程序能将用户的新口令写入口令文件,只有root才具有此文件的写权限。
设置用户ID和设置组ID位包含在st_mode中,可将st_mode与常量S_ISUID和S_ISGID相与测试。
st_mode也包含了对文件的访问权限位,所有文件类型都有访问权限。
当我们用路径想打开一个文件时,我们必须有该路径上任何目录的执行权限,包括该文件所在的目录,这也是为什么我们叫目录的执行位为搜索位。
目录的读权限和执行权限是不同的,读权限能使我们得到目录中的文件名,而执行权限使我们能通过路径上的目录名来访问该文件。
如果PATH环境变量指定了一个我们不具有执行权限的目录,shell不会在该目录下找到可执行文件。
以O_TRUNC标志调用open,必须对文件有写权限。
为了在目录中创建新文件,必须对目录有写权限和执行权限。
为删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限,对该文件本身不需要读写权限。
如果用exec函数执行某文件,必须对该文件有执行权限,该文件必须是普通文件。
进程在打开、创建或删除文件时,内核按以下顺序测试程序的访问权限:
1.root(有效用户ID为0)具有任何权限。
2.进程的有效用户ID等于文件的所有者ID,那么按文件的所有者的权限来确定进程权限。
3.进程的有效组ID或进程的附属组ID是否等于文件的组ID,如果是,则按组权限来确定进程权限。
4.按其他用户的权限来确定进程权限。
如果进程拥有第二步权限,则不查看第三步组访问权限。类似地,如果进程拥有第三步的权限,则不看其他用户的访问权限。
用open或creat函数创建一个文件时,文件的用户ID设置为进程的有效用户ID,对于组ID,POSIX.1允许实现选择以下选项:
1.新文件组ID是创建它的进程的有效组ID。
2.新文件组ID是它所在目录的组ID。
对于Linux 3.2.0和Solaris 10,默认新文件的组ID取决于它所在目录的设置组ID是否被设置,如果被设置,新文件的组ID是目录的组ID,否则,新文件组ID是进程的有效组ID。
使用第二个选项时,某个目录下创建的文件和目录都具有该目录的组ID,于是文件和目录的组所有权从该点向下传递,如Linux上的/var/mail目录就使用了该方法。
如果希望按进程的实际用户ID和实际组ID测试其访问能力,而非open函数这样以有效用户ID和有效组ID进行测试访问能力的测试,可使用以下函数测试:
如果测试文件是否存在,mode参数为F_OK,如果测试已存在文件的访问权限:
测试访问权限的顺序与上述用open或creat函数创建一个文件的检测顺序一致,只不过测试的是实际ID而非有效ID。
当faccessat函数的pathname参数为绝对路径或fd参数为AT_FDCWD且pathname为相对路径时,faccessat和access函数相同。
函数faccessat的flag参数为AT_EACCESS时,访问权限检查用的是调用进程的有效用户ID和有效组ID。
使用access函数:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("usage: a.out <pathname>\n");
exit(1);
}
if (access(argv[1], R_OK) < 0) {
err_ret("access error for %s\n", argv[1]);
} else {
printf("read access OK\n");
}
if (open(argv[1], O_RDONLY) < 0) {
err_ret("open error for %s\n", argv[1]);
} else {
printf("open for reading OK\n");
}
exit(0);
}
以上代码中的err_ret函数作用为打印参数字符串,并后跟一个冒号和错误信息。
运行它:
umask函数为进程设置文件模式创建屏蔽字,并返回之前的值:
参数cmask是图4-6中mode_t的9个权限常量的或。
在文件模式创建屏蔽字中为1的位,在open或creat函数创建文件时该位一定被关闭。
使用umask函数:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)
int main() {
umask(0);
if (creat("foo", RWRWRW) < 0) {
printf("creat error for foo\n");
exit(1);
}
umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if (creat("bar", RWRWRW) < 0) {
printf("creat error for bar\n");
exit(1);
}
exit(0);
}
运行它:
如果bar和foo文件在程序运行前已经存在,则文件会被截断,但文件权限保持执行程序前不变。
用户的umask值可用命令umask查看,在登录时,由shell的启动文件设置一次,当编写创建新文件的程序时,如果我们想确保指定的访问权限位已经激活,就必须在程序运行时修改umask值。
改变umask的程序运行后不影响父进程(shell)的屏蔽字。
用户可设置umask值以控制创建文件的默认权限,该值为八进制数,设置了相应位后,它所对应的权限就会被拒绝:
SUS要求shell应支持符号形式的umask命令,符号形式指定许可的权限而非要屏蔽的权限:
改变现有文件的访问权限:
chmod函数在参数pathname指定的文件上操作,而fchmod函数对已打开的文件进行操作。fchmodat和chmod函数在以下条件下是相同的:
1.pathname参数是绝对路径。
2.fchmodat的fd参数取值为AT_FDCWD且pathname参数为相对路径。
fchmodat函数在其他情况下计算相对于由参数fd表示的打开目录的参数pathname。flag参数为AT_SYMLINK_NOFOLLOW时改变的是符号链接的权限。
为改变文件的权限位,进程的有效用户ID必须等于文件所有者ID或进程有root权限。
参数mode的取值:
改变以上两文件的权限:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
int main() {
struct stat statbuf;
if (stat("foo", &statbuf) < 0) {
printf("stat error for foo\n");
exit(1);
}
if (chmod("foo", (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0) {
printf("chmod error for foo\n");
exit(1);
}
if (chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) < 0) {
printf("chmod error for bar\n");
exit(1);
}
exit(0);
}
执行它:
设置组ID位或设置用户ID位体现在S,如果同时也开启了执行位,则会变为s。
在Solaris中,ls命令会显示l而非S,表明可对文件加强制性文件或记录锁。
在执行完以上程序后,文件的时间和日期并未改变,chmod函数更新的只是i节点最近一次被更新的时间,而ls -l列出的是最后文件被修改的时间。
chmod函数的特殊情况:
1.Solaris、FreeBSD等系统上对普通文件的粘着位赋予了特殊含义,如果我们在这些系统上试图设置普通文件的粘着位,但没有root权限,则粘着位会自动关闭。这些系统上只有root才能设置普通文件的粘着位,理由是防止恶意用户设置粘着位,从而影响系统性能。Linux和Mac上无此限制,因为粘着位对Linux普通文件无意义,虽然粘着位对FreeBSD的普通文件也无意义,但还是阻止除root外的用户设置普通文件的粘着位。
2.新创建文件的组ID可能不是调用进程所属的组,新文件组ID可能是父目录的组ID(如果目录的设置组ID位开启)。如果新创建的文件的组ID不等于进程的有效组ID或附属组ID,且进程没有root权限,则新建文件的设置组ID位会被自动关闭,这防止了用户创建一个设置组ID的文件但该文件并非该用户所属的组拥有。
很多系统增加了新机制阻止误用设置ID位,如果没有root权限的进程写一个文件,则该文件的设置用户ID位和设置组ID位会自动清除,防止恶意用户找到一个他们可以写的设置组ID或设置用户ID的文件,从而修改文件内容,这样即使可以修改文件,也没有对该文件的特殊权限。
在UNIX还未使用请求分页式技术的早期版本,S_ISVTX称为粘着位,如果可执行程序的此位被设置,则程序第一次被执行并终止后,程序正文部分(机器指令)的一个副本仍被保存在交换区,使得下次运行该程序时能较快地将其载入内存。原因是通常的UNIX系统的文件各数据块是随机存放的,交换区是被作为一个连续文件来处理的,对于通用的应用常设置其粘着位。交换区可存放的设置了粘着位的文件数有限制,以免过多占用交换区空间。在系统再次自举前,文件的正文部分总是在交换区空间,后来的UNIX版本称它为保存正文位(saved-text bit),因此也有了常量S_ISVTX,现今系统都配置了虚拟存储系统和快速文件系统,也不再需要使用这种技术。
现今系统扩展了粘着位使用范围,SUS允许针对目录设置粘着位,作用为只有对该目录具有写权限且满足以下条件之一,才能删除或重命名该目录下的文件(没有粘着位时,只要对目录有权限就能删除其中的文件):
1.拥有此文件。
2.拥有此目录。
3.是root用户。
目录/var和/var/tmp设置了粘着位,任何用户都能在这两个目录中创建文件,目录权限为777,但用户不能删除或重命名其他人的文件。
设置了粘着位时,ls命令会在其他人的权限位处显示t:
Solaris 10中,如果对普通文件设置了粘着位,但任何执行位(x)都没有设置,那么操作系统不会缓存文件内容。
改变文件的用户ID和组ID的函数:
如果参数owner为-1,则文件所属的UID不变,参数group同理。
以上几个函数在参数为符号链接的情况下,lchown函数和设置了AT_SYMLINK_NOFOLLOW的fchownat函数更改符号链接本身。
fchown函数改变fd参数指向的打开文件的所有者。
fchownat函数的参数pathname为绝对路径或fd参数为AT_FDCWD且pathname参数为相对路径时:
1.如果参数flag设置了AT_SYMLINK_NOFOLLOW,则行为与lchown函数相同。
2.否则与函数chown行为相同。
如果函数chownat函数的参数fd是打开目录的文件描述符,且pathname参数是相对路径名,则函数计算相对于打开目录的pathname。
基于BSD的系统只有超级用户才能更改文件所有者,原因是防止用户改变其文件的所有者从而摆脱磁盘空间限额对其的限制。System V允许任一用户更改其所拥有文件的所有者。而POSIX.1按照_POSIX_CHOWN_RESTRICTED的值二者选一。
_POSIX_CHOWN_RESTRICTED可设置对单个文件是否生效,如生效:
1.只有root用户进程才能改变文件的用户ID。
2.如果进程没有root权限,如果进程拥有此文件(进程的有效用户ID等于文件的用户ID),参数owner只能等于-1或文件的用户ID,而参数group能等于进程的有效组ID或进程的附属组ID之一,即该常量生效时,不能更改用户文件的用户ID,且只能将文件组ID改到你所属的组。
如果改变所有者的函数由非root进程调用,则成功返回时,文件的设置用户ID位和设置组ID位将被清除。
stat结构的成员st_size表示以字节为单位的文件长度,此字段只对普通文件、目录文件和符号链接有意义。有些系统中管道也定义了文件长度,表示可从管道中读到的字节数。
普通文件的长度可以是0,开始读这种文件时,将得到EOF。目录的文件长度通常是一个数的整数倍。符号链接的长度是文件名的字节数,如下:
文件长度为7(usr/lib的长度)。
大多UNIX系统都提供字段st_blksize(对文件IO较合适的块长)和st_blocks(所分配的512字节块的数量,不同的UNIX实现中该值可能不是512字节),为提高效率,标准IO库每次试图读写st_blksize个字节。
普通文件可以包含空洞,空洞是由于设置的偏移量超过文件尾端,并写入了数据造成的:
文件core长度稍稍超过8M,而du命令报告该文件使用的磁盘空间总量是272个512字节块,即139264字节(0.139M字节),文件中有空洞。
du命令报告的块的大小取决于系统,Linux块大小取决于环境变量POSIXLY_CORRECT,设置了该环境变量时,du命令报告1024字节大小的块的块数,否则报告512字节大小的块的块数。
当read函数读的位置没有被写过时,读到的字节为0,查看可读的文件长度:
wc -c
表示文件中的字节数。
如果使用以下方式复制文件,则空洞会被填充为0:
可见新文件所用实际字节数为8495104(512*16592),与ls命令报告的长度不符,原因是文件系统使用了若干块以存放指向实际数据块的各个指针。
以上两文件的权限不同,原因是内核在创建core文件时,有一个默认的文件权限位的设置( 此例中是rw-r–r--),该值可能会也可能不会被umask的值所修改;而shell在创建新文件(core.copy)时,也有默认的文件权限位(此例中是 rw-rw-rw-),该权限位永远会被umask的值所(本例中为0002)修改,因此其他人写权限被屏蔽。
在文件尾截去指定大小的数据:
这两个函数将一个现有文件长度截断为参数length,如果文件以前的长度大于length,则超过length外的数据不能再访问,如果之前的长度小于length,文件长度将增加,以前的文件尾端和新的文件尾端之间的数据读作0(即可能在文件中创建了空洞)。
UNIX文件系统有多种实现,下面讨论UFS(传统的基于BSD的UNIX文件系统)。
Mac上文件名是不区分大小写的。
我们可以把一个磁盘分成一个或多个分区,每个分区可以包含一个文件系统,i节点是固定长度的记录项,包含有关文件的大部分信息。
上图中有两个目录项指向同一个i节点,每个i节点有一个链接计数,其值是指向该i节点的目录项数,只有当链接计数减到0,才能删除该文件(释放该文件占用的磁盘块)。这也是为什么解除对一个文件的链接不总是意味着释放该文件占用的磁盘块。也是为什么删除一个目录项的函数名为unlink而非delete。链接计数存放在stat结构的st_nlink成员中,其基本数据类型是nlink_t,这种链接类型称为硬链接。POSIX.1常量LINK_MAX指定了一个文件链接数的最大值。
另一种链接被称为符号链接(软链接),符号链接文件的实际内容(数据块内容)包含了该符号链接所指向的文件的名字。如下例,目录项中文件名是3个字符的lib,文件中有7字节的数据(usr/lib):
符号链接的i节点中的文件类型为S_IFLNK。
i节点包含了文件有关的所有信息:文件类型、文件访问权限位、文件长度、指向文件数据块的指针等。stat结构中大多数信息取自i节点,只有两项内容存放在目录项中:文件名和i节点编号。i节点编号的数据类型为ino_t。
目录项不能指向另一个文件系统的i节点,这也是为什么ln命令不能跨文件系统的原因。
不更换文件系统情况下为一个文件重命名时,该文件实际内容未移动,只需构造一个现有i节点的新目录项,并删除老目录项,链接计数不会改变。这是mv命令的通常操作方式。
上图是创建了一个目录testdir后的情况。编号2549的i节点的文件类型字段表明它是一个目录,链接计数为2,任何一个叶目录(不包含其它目录的目录)的链接计数总是2,分别来自该目录的目录项(其父目录中的目录项)和该目录中的.项。编号为1267的i节点的类型字段也表明其是一个目录,其链接计数大于等于3,分别来自其父目录中的目录项、该目录中的.项、其子目录testdir中的…项。父目录中的每个子目录都使该父目录的链接计数+1。
创建一个指向现有文件的链接的函数:
这两个函数创建一个新目录项,新目录项路径为参数newpath,它引用一个路径为参数existingpath的现有文件。
linkat函数的现有文件是通过参数efd和existingpath指定的,新文件的路径名是通过参数nfd和newpath指定的。默认,对于两个路径名,如果是相对路径,则相对于对应的文件描述符计算;如果文件描述符为AT_FDCWD,则相应的路径名如果是相对路径,则相对于当前目录进行计算,如果路径名是绝对路径,则对应的文件描述符参数会被忽略。
linkat函数的现有文件是符号链接时,参数flag如果是AT_SYMLINK_FOLLOW则创建指向符号链接目标的链接,否则创建指向符号链接本身的链接。
创建新目录项和增加链接计数是一个原子操作。
POSIX.1允许实现跨越文件系统的链接,但大多实现不允许。如果实现支持创建指向一个目录的硬链接,也仅限于超级用户这么做,因为这可能在文件系统中形成循环,而大多处理文件系统的实用程序不能处理此情况,因此很多文件系统实现不允许对于目录的硬链接。
删除一个现有目录项:
这两个函数将由pathname参数引用文件的链接计数减1。如果函数出错,则不对文件做任何修改。
为解除对文件的链接,需要对包含该目录项的目录具有写和执行权限;如目录设置了粘着位,则对该目录必须有写权限,还必须符合以下条件之一:
1.拥有该文件。
2.拥有该目录。
3.具有root权限。
只有链接计数达到0,文件内容才能被删除。有进程打开了该文件时,也不能删除该文件内容。关闭一个文件时,内核首先检查打开该文件的进程个数,如果为0,再去检查其链接计数,如果也为0,则删除该文件内容。
如果参数pathname是相对路径名,则unlinkat函数计算相对于由fd参数代表的目录的路径名。如果fd参数为AT_FDCWD,那么相对于进程当前工作目录来计算路径名;如果参数pathname为绝对路径,则fd参数被忽略。
unlinkat函数的flag参数为AT_REMOVEDIR时,可像rmdir函数一样删除目录。
打开一个文件,然后解除它的链接:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
if (open("tempfile", O_RDWR) < 0) {
printf("open error\n");
exit(1);
}
if (unlink("tempfile") < 0) {
printf("unlink error\n");
exit(1);
}
printf("file unlinked\n");
sleep(15);
printf("done\n");
exit(0);
}
运行它:
unlink函数的这种特性常被用来确保即使是在程序崩溃时,它所创建的临时文件也不会遗留下来,具体做法是程序用open或creat函数创建一个文件,然后立即对其调用unlink,因为文件依旧是打开的,所以不会将其内容删除,只有当进程关闭该文件或终止(此时内核关闭该进程所有打开文件)时,该文件内容才会被删除。
上图中使用命令df而非du查看空间是由于du命令需要文件名才能获取大小,如果使用du .
获取当前目录中文件大小,则unlink tempfile后由于文件名已不存在此目录中,从而不会获取该文件大小。
如果pathname参数是符号链接,那么unlink函数删除该符号链接,而非删除由链接引用的文件。给出符号链接名的情况下,任何函数都不能删除该链接所引用的文件。
如果文件系统支持,超级用户可以调用unlink删除一个目录,但通常应使用rmdir函数。
可用remove函数解除对一个文件或目录的链接,对于文件,相当于使用unlink函数,对于目录,相当于使用rmdir函数:
ISO C执行remove函数删除文件而非使用unlink函数,原因是实现C标准的大多非UNIX系统不支持文件链接。
重命名文件或目录(相当于mv命令):
ISO C对文件定义了rename函数,而C标准不处理目录,POSIX.1扩展了此定义,使其能处理目录和符号链接。
oldname参数:
1.是文件时,则为该文件或符号链接重命名。此时,如果newname已存在,则它不能是一个目录,如果不是目录,则会将newname指定的目录项删除然后再将oldname重命名为newname。对包含oldname的目录和newname的目录,进程必须有写权限,因为要更改目录。
2.是目录时,如果newname已存在,则它必须是一个空目录,此时会将该目录删除,再将oldname重命名为newname。当为一个目录重命名时,不能将/usr/foo改名为/usr/foo/testdir,因为旧名字是新名字的路径前缀。
3.如果oldname或newname是符号链接,则处理的是符号链接本身。
4.不能重命名.
和..
。
5.如果oldname和newname是同一文件,则函数不做任何修改成功返回。
如果newname已经存在,则进程需对它有写权限。
由于进程会删除oldname的目录项,创建新的newname目录项,因此进程需要对这两个参数表示的文件所在的目录有写和执行权限。
符号链接不像硬链接直接指向文件的i节点,而是一个文件的间接指针,它只要是为了避开硬链接的一些限制:
1.硬链接通常要求链接和文件位于同一文件系统。
2.只有root才能创建指向目录的硬链接(底层文件系统支持的情况下)。
符号链接和它指向何种对象没有任何文件系统限制,任何用户都能创建指向目录的符号链接。符号链接一般用于将一个文件或整个目录结构移动到系统中的另一位置。
使用以名字引用文件的函数时,应了解该函数是否处理符号链接,即是否跟随符号链接到达它所链接的文件:
未列出mkdir、mkinfo、mknod、rmdir函数,因为当路径名是符号链接时,它们出错返回。
上图的一个例外是同时用O_CREAT和O_EXCL调用open时,此时如果路径名引用符号链接,open函数将出错返回,errno被设为EEXIST,这堵塞了一个安全性漏洞,防止具有特权的进程被诱骗写错误的文件。
符号链接可能在文件系统中引入循环,大多查找路径名的函数此时会出错返回,并将errono置为ELOOP:
如果使用Solaris的标准函数ftw降序遍历文件结构,打印遇到的文件名:
Linix的ftw和nftw函数记录了所有看到的目录并避免多次重复出现同一目录,因此不会出现以上行为。
这种循环很容易消除,unlink函数不跟随符号链接,该函数可直接删除该符号链接,但如果创建了构成循环的硬链接,很难消除,这也是为什么link函数不允许构造指向目录的硬链接(除非进程有root权限)。
open函数打开符号链接时,会跟随此链接到达指定的文件,若此符号链接指向的文件不存在,则open函数返回出错:
ls -l
命令显示的条目中,第一个字符是l,表示这是一个符号链接,而->
也证明如此。ls命令的-F选项会在符号链接文件名后加一个@符号,未使用-l选项时,这样可以让我们识别出符号链接。
创建符号链接:
函数创建了一个指向actualpath的新目录项sympath,创建此符号链接时,并不要求actualpath已存在,actualpath和sympath不需要位于同一文件系统中。
symlinkat函数与symlink函数类似,其sympath参数根据相对于打开文件描述符所引用的目录(fd参数)计算,如果sympath参数指定的是绝对路径或fd为AT_FDCWD,则symlinkat函数等同于symlink函数。
打开符号链接文件本身:
以上两函数组合了open、read、clsoe函数的所有操作,如果函数成功执行,则返回读入buf的字节数,在buf中返回的符号链接内容不以null字节结尾。
当函数readlinkat的参数pathname指定的是绝对路径名或fd参数的值为AT_FDCWD时,其行为与函数raedlink相同,否则计算相对于打开文件描述符fd表示的目录的pathname参数。
每个文件属性中保存的时间的实际精度依赖于文件系统的实现。对于把时间戳记录在秒级的文件系统来说,纳秒字段会被填充为0。
每个文件维护三个时间字段:
系统不维护最后一次访问一个i节点的时间。因此stat、access函数并不改变以上三个时间中的任一个。
文件维护的时间通常被系统管理员用来删除一定时间内没有被访问过的文件,如过去一周内没有被访问过的a.out或core文件。find命令常被用来执行此操作。
ls命令的-l或-t选项是按文件的修改时间的先后排序显示(我测试时-l选项是按文件名字典顺序排的),-u选项是按文件被访问时间排序,-c选项按i节点状态更改时间排序。
目录是包含目录项的文件,增加、删除、修改目录项会影响到它所在目录相关的三个时间。创建一个文件影响到包含文件的目录,也影响该新文件的i节点。但读写一个文件只影响该文件的i节点。
上图中显示unlink函数会改变文件的文件状态更改时间,这是因为如果一个文件有多个链接,则调用unlink后会减少i节点的引用计数。
更改文件的访问和修改时间,指定纳秒级精度的时间戳:
函数的times数组参数的第一个元素是访问时间,第二个元素是修改时间,这两个时间值是日历时间,这是自1970.1.1 00:00:00以来经过的秒数,不足秒的部分用纳秒表示。
times参数为:
1.空指针时,访问时间和修改时间设为当前时间。此时进程的有效用户ID必须等于文件的所有者ID,进程对该文件必须具有写权限,或进程有root权限。
2.指向timespec结构的数组,任一元素的tv_nsec字段为UTIME_NOW,则相应时间戳设为当前时间,忽略tv_sec字段。此时进程的有效用户ID必须等于文件的所有者ID,进程对该文件必须具有写权限,或进程有root权限。
3.指向timespec结构的数组,任一元素的tv_nsec为UTIME_OMIT,则相应时间戳保持不变,忽略相应的tv_sec字段。此时不进行权限检查。
4.指向timespec结构的数组,任一元素的tv_nsec既不为UTIME_NOW也不为UTIME_OMIT,则设置相应的时间戳。此时进程的有效用户ID必须是文件的所有者,或进程是root进程,对文件有写权限。
utimensat函数根据参数fd指定的打开目录计算path参数。
utimensat函数的flag参数为AT_SYMLINK_NOFOLLOW时,符号链接本身会被修改,否则跟随符号链接,更改其指向的文件的时间。
以上两函数包含在POSIX.1中,以下函数包含在SUS的XSI扩展中:
此函数的两个时间戳(访问时间和修改时间)是用秒和微秒表示的:
我们不能使用以上函数修改i节点最近被修改的时间,因为调用它时,此字段自动被更新。当我们想修改文件的访问时间和文件内容的修改时间其中一个时,可先用stat函数获取文件的时间值,然后不想改变的值用所得的值调用utimes即可。
某些版本UNIX中,touch命令使用这些函数之一。标准归档程序tar和cpio可选地调用这些函数,以便将文件的时间值设置为将它归档的时间。
使用带O_TRUNC的open函数将文件长度截短到0,但不更改其访问时间和修改时间:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int i, fd;
struct stat statbuf;
struct timespec times[2];
for (i = 1; i < argc; ++i) {
if (stat(argv[i], &statbuf) < 0) {
printf("%s: stat error\n", argv[i]);
continue;
}
if ((fd = open(argv[i], O_RDWR | O_TRUNC)) < 0) {
printf("%s: open error\n", argv[i]);
continue;
}
times[0] = statbuf.st_atim;
times[1] = statbuf.st_mtim;
if (futimens(fd, times) < 0) {
printf("%s: futimens error\n");
}
close(fd);
}
exit(0);
}
运行它:
创建目录:
新目录中的.
和..
是自动创建的,参数mode指定的文件访问权限会被进程的文件模式创建屏蔽字修改。
对目录至少要设置一个执行权限位,以允许访问该目录中的文件名。
Solaris 10和Linux 3.2.0使新目录继承父目录的设置组ID位,这使得新目录中创建的文件也继承该目录的组ID。在Linux中,文件系统的实现决定是否支持此特征,如ext2、ext3、ext4文件系统用mount命令的一个选项支持该特征,但UFS(Unix File System)新目录直接继承父目录的设置组ID位。
基于BSD的系统(如FreeBSD 8.0和Mac OS X 10.6.8)不要求在目录间传递设置组ID位,因为无论设置组ID位如何,新创建的文件和目录总是继承父目录的组ID。
早期UNIX版本没有mkdir函数,进程调用mknod创建新目录,但只有root用户能使用mknod函数,为了让普通用户也能创建目录,mkdir命令要由root用户所有,且设置了设置用户ID位,这样普通用户可通过system函数调用mkdir命令来创建一个目录。
mkdirat函数的fd参数为AT_FDCWD或pathname参数指定了绝对路径时,mkdirat和mkdir函数完全一样,否则,相对路径名根据参数fd指定的打开目录计算。
删除空目录:
此函数使目录的链接计数成为0,如果没有其他进程打开此目录,则释放此目录占用的空间,如果有其他进程打开了目录,则此函数返回前会删除最后一个链接及.
和..
项,但在最后一个进程关闭它前不释放此目录,这样就算有进程还打开该目录,它们在此目录下也不能执行太多操作,这样目录可以保持空,使得rmdir函数成功执行。
对目录有读权限的用户可以读目录,但为防止文件系统产生混乱, 只有内核才能写目录。
目录的实际格式取决于UNIX系统实现和文件系统设计,早期系统每个目录项大小固定,但后来的系统支持更长的文件名,因此每个目录项大小不确定,这意味着读目录的程序与系统有关,因此有一套与读目录有关的例程。很多实现阻止应用用read函数读取目录内容,这进一步将应用程序与目录格式中与实现相关的细节隔离:
fdopendir函数可将打开文件描述符转换成目录处理函数需要的DIR结构。
头文件dirent.h中的dirent结构与实现有关,其至少要包含以下成员:
d_name项的大小没有指定,但它必须能包含NAME_MAX+1个字节(NAME_MAX表示最大文件名,不含null字节,因此d_name至少能容纳NAME_MAX+1字节)。因为文件名是以null字节结尾,所以在头文件中如何定义数组大小并无关系,数组大小不代表文件名长度。
DIR结构是一个内部结构,上述函数使用该内部结构保存当前正在被读的目录的有关信息。
opendir函数返回初始化的DIR结构指针,第一个readdir函数返回目录中的第一个目录项。而fdopendir函数创建的DIR结构,第一个readdir函数返回的第一项取决于传给fdopendir函数的文件描述符相关联的文件偏移量。目录中目录项的顺序与实现有关,通常不按字母顺序排。
Solaris提供的遍历文件层次结构的函数ftw存在以下问题:它对于每个文件,都调用stat函数,这使得程序跟随符号链接,从而一个文件会被计数两次。虽然Solaris提供了nftw函数,它具有一个停止跟随符号链接的选项。我们编写了一个简单的文件遍历程序:
#include <dirent.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <unistd.h>
typedef int Myfunc(const char *, const struct stat *, int);
static Myfunc myfunc;
static int myftw(char *, Myfunc *);
static int dopath(Myfunc *);
static long nreg, ndir, nblk, nchr, nfifo, nslink, nsock, ntot;
#ifdef PATH_MAX
static long pathmax = PATH_MAX;
#else
static long pathmax = 0;
#endif
static long posix_version = 0;
static long xsi_version = 0;
#define PATH_MAX_GUESS 1024 // if PATH_MAX is indeterminate, no guarantee this is adequate
char *path_alloc(size_t *sizep) {
char *ptr;
size_t size;
if (posix_version == 0) {
posix_version = sysconf(_SC_VERSION);
}
if (xsi_version == 0) {
xsi_version = sysconf(_SC_XOPEN_VERSION);
}
if (pathmax == 0) { // first time through
errno = 0;
if ((pathmax = pathconf("/", _PC_PATH_MAX)) < 0) {
if (errno == 0) {
pathmax = PATH_MAX_GUESS;
} else {
printf("pathconf error for _PC_PATH_MAX");
}
} else {
++pathmax; // add one since it's relative to root
}
}
// Before POSIX.1-2001, we aren't guaranteed that PATH_MAX includes the terminating null byte.
// Same goes for XPG3
if ((posix_version < 200112L) && (xsi_version < 4)) {
size = pathmax + 1;
} else {
size = pathmax;
}
if ((ptr = malloc(size)) == NULL) {
printf("malloc error for pathname\n");
exit(1);
}
if (sizep != NULL) {
*sizep = size;
}
return ptr;
}
int main(int argc, char *argv[]) {
int ret;
if (argc != 2) {
printf("usage: ftw <starting-pathname>\n");
exit(1);
}
ret = myftw(argv[1], myfunc);
ntot = nreg + ndir + nblk + nfifo + nslink + nsock;
if (ntot == 0) { // avoid divid by 0
ntot = 1;
// printf("nton is 0\n");
}
printf("regular files = %7ld, %5.2f %%\n", nreg, nreg * 100.0 / ntot);
printf("directories = %7ld, %5.2f %%\n", ndir, ndir * 100.0 / ntot);
printf("block special = %7ld, %5.2f %%\n", nblk, nblk * 100.0 / ntot);
printf("char special = %7ld, %5.2f %%\n", nchr, nchr * 100.0 / ntot);
printf("FIFOs = %7ld, %5.2f %%\n", nfifo, nfifo * 100.0 / ntot);
printf("symbolic links = %7ld, %5.2f %%\n", nslink, nslink * 100.0 / ntot);
printf("sockets = %7ld, %5.2f %%\n", nsock, nsock * 100.0 / ntot);
exit(ret);
}
#define FTW_F 1 // file other than directory
#define FTW_D 2 // directory
#define FTW_DNR 3 // directory that cat't be read
#define FTW_NS 4 // file that we cat't stat
static char *fullpath;
static size_t pathlen;
static int myftw(char *pathname, Myfunc *func) {
fullpath = path_alloc(&pathlen); // don't allocate memory with size pathlen, pathlen's value is determined by path_alloc
if (pathlen <= strlen(pathname)) {
pathlen = strlen(pathname) * 2;
if ((fullpath = realloc(fullpath, pathlen)) == NULL) {
printf("realloc failed\n");
exit(1);
}
}
strcpy(fullpath, pathname);
return dopath(func);
}
static int dopath(Myfunc *func) {
printf("In dopath\n");
struct stat statbuf;
struct dirent *dirp;
DIR *dp;
int ret, n;
if (lstat(fullpath, &statbuf) < 0) { // stat error
return func(fullpath, &statbuf, FTW_NS);
}
if (S_ISDIR(statbuf.st_mode) == 0) { // not a directory
return func(fullpath, &statbuf, FTW_F);
}
// now it's a directory
if ((ret = func(fullpath, &statbuf, FTW_D)) != 0) {
return ret;
}
n = strlen(fullpath);
if (n + NAME_MAX + 2 > pathlen) {
pathlen *= 2;
if ((fullpath = realloc(fullpath, pathlen)) == NULL) {
printf("realloc failed\n");
exit(1);
}
}
fullpath[n++] = '/';
fullpath[n] = 0;
if ((dp = opendir(fullpath)) == NULL) { // can't read directory
return func(fullpath, &statbuf, FTW_DNR);
}
while ((dirp = readdir(dp)) != NULL) {
if (strcmp(dirp->d_name, ".") == 0 || strcmp(dirp->d_name, "..") == 0) { // ignore dot and dot-dot
continue;
}
strcpy(&fullpath[n], dirp->d_name); // append name after /
if ((ret = dopath(func)) != 0) { // recursive, deep-first search
// 注意此处的递归深度(目录树的深度)受系统可打开的文件数限制
break; // time to leave
}
}
fullpath[n - 1] = 0; // erase everything from slash onward
if (closedir(dp) < 0) {
printf("can't close directory %s\n", fullpath);
}
return ret;
}
static int myfunc(const char *pathname, const struct stat *statptr, int type) {
switch (type) {
case FTW_F:
switch (statptr->st_mode & S_IFMT) { // macro defined in sys/stat.h
case S_IFREG: nreg++; break;
case S_IFBLK: nblk++; break;
case S_IFCHR: nchr++; break;
case S_IFIFO: nfifo++; break;
case S_IFLNK: nslink++; break;
case S_IFSOCK: nsock++; break;
case S_IFDIR:
printf("for S_IFDIR for %s\n", pathname);
}
break;
case FTW_D:
++ndir;
break;
case FTW_DNR:
printf("can't read directory %s\n", pathname);
return 0;
break;
case FTW_NS:
printf("stat error for %s\n", pathname);
return 0;
break;
default:
printf("unknown type %d for pathname %s\n", type, pathname);
}
return 0;
}
执行它:
虽然myfunc函数总是返回0,但调用它的函数准备了处理非0返回。这是为了程序增加通用性。
每个进程都有一个当前工作目录,此目录是相对路径名的起点,用户登录到UNIX系统时,当前工作目录通常是口令文件/etc/passwd中登录项的第6个字段----用户的起始目录。
更改当前工作目录:
以上函数分别用pathname或打开文件描述符来指定新的当前工作目录。
当前工作目录是进程的一个属性,因此它只影响调用chdir的进程本身。因此以下操作不会达到我们希望的结果:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main() {
if (chdir("/tmp") < 0) {
printf("chdir failed\n");
exit(1);
}
printf("chdir to /tmp successed\n");
exit(0);
}
调用以上程序:
shell执行程序时,每个程序运行在独立的进程中。为了改变shell自己的工作目录,shell应直接调用chdir函数,为此,cd命令内嵌在shell中。
内核要维护当前工作目录的信息,我们应能获取其值,但内核为每个进程只保存指向当前工作目录的v节点的指针等目录信息,但不保存当前工作目录的完整路径。(Linux内核可确认完整路径名,完整路径名的各个组成部分分布在mount表和dcache表中,然后重新组装)
我们需要一个函数从当前目录开始,一层一层找上一级目录,读取目录中的目录项,直到遇到根,就得到了当前工作目录的完整绝对路径名:
此函数的缓冲区buf的长度size必须能装下绝对路径名和一个空字节,否则出错返回NULL。
将工作目录更改至某个指定目录,然后调用getcwd,最后打印该工作目录:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#ifdef PATH_MAX
static long pathmax = PATH_MAX;
#else
static long pathmax = 0;
#endif
static long posix_version = 0;
static long xsi_version = 0;
#define PATH_MAX_GUESS 1024 // if PATH_MAX is indeterminate, no guarantee this is adequate
char *path_alloc(size_t *sizep) {
char *ptr;
size_t size;
if (posix_version == 0) {
posix_version = sysconf(_SC_VERSION);
}
if (xsi_version == 0) {
xsi_version = sysconf(_SC_XOPEN_VERSION);
}
if (pathmax == 0) { // first time through
errno = 0;
if ((pathmax = pathconf("/", _PC_PATH_MAX)) < 0) {
if (errno == 0) {
pathmax = PATH_MAX_GUESS;
} else {
printf("pathconf error for _PC_PATH_MAX");
}
} else {
++pathmax; // add one since it's relative to root
}
}
// Before POSIX.1-2001, we aren't guaranteed that PATH_MAX includes the terminating null byte.
// Same goes for XPG3
if ((posix_version < 200112L) && (xsi_version < 4)) {
size = pathmax + 1;
} else {
size = pathmax;
}
if ((ptr = malloc(size)) == NULL) {
printf("malloc error for pathname\n");
exit(1);
}
if (sizep != NULL) {
*sizep = size;
}
return ptr;
}
int main() {
char *ptr;
size_t size;
if (chdir("/usr/spool/uncppublic") < 0) {
printf("chdir failed\n");
exit(1);
}
ptr = path_alloc(&size);
if (getcwd(ptr, size) == NULL) {
printf("getcwd failed\n");
exit(1);
}
printf("cwd = %s\n", ptr);
exit(0);
}
执行它:
可见chdir函数会跟随符号链接。
我们可以先调用getcwd返回当前目录,之后可以随意更换目录,之后可以用该返回值调用chdir,就可以回到原目录。使用fchdir函数也可以完成该任务,使用open打开当前目录,然后保存返回的文件描述符,之后使用fchdir函数即可返回原目录。
st_dev(文件系统的设备号)和se_rdev(特殊文件的设备号)的区别:
1.每个文件系统所在的存储设备都由其主、次设备号表示。设备号用的数据类型是基本系统数据类型dev_t。主设备号标识设备驱动程序,有时编码为与其通信的外设板;次设备号标识特定的子设备。一个磁盘驱动器经常包含若干个文件系统,在同一磁盘驱动器上的各文件系统通常具有相同的主设备号,但次设备号不同。
2.我们常用两个宏函数major、minor来访问主、次设备号,我们无需关心这两个数是如何存放在dev_t对象中的。这两个宏包含在哪个头文件取决于具体实现,Linux将它们定义在sys/sysmacros.h中,而该头文件又包含在sys/type.h中。
3.系统中与每个文件名关联的st_dev值是文件系统的设备号,该文件系统包含了此文件名以及与其对应的i节点。
4.只有字符特殊文件和块特殊文件才有st_rdev值,该值包含实际设备的设备号。
为每个命令行参数打印设备号,如此参数引用的是字符特殊文件或块特殊文件,则打印该特殊文件的st_rdev值:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
// In solaris, major and minor is defined in sys/mkdev.h
#ifdef SOLARIS
#include <sys/mkdev.h>
#endif
int main(int argc, char *argv[]) {
int i;
struct stat buf;
for (i = 1; i < argc; ++i) {
printf("%s: ", argv[i]);
if (stat(argv[i], &buf) < 0) {
printf("stat error\n");
continue;
}
printf("dev = %d/%d", major(buf.st_dev), minor(buf.st_dev));
if (S_ISCHR(buf.st_mode) || S_ISBLK(buf.st_mode)) {
printf(" (%s) rdev = %d/%d", (S_ISCHR(buf.st_mode) ? "charactor" : "block"), major(buf.st_rdev), minor(buf.st_rdev));
}
printf("\n");
}
exit(0);
}
在Linux上运行该程序:
根目录和/home/sar的设备号不同,说明它们在两个不同的文件系统,mount证明了这点。ls命令说明磁盘设备是块特殊文件,通常只有可以包含随机访问文件系统的设备的类型是块特殊文件设备。
两个终端设备的文件名和i节点都在设备5上,但它们的实际设备号是4/0和4/1。
目录的执行权限影响搜索包含该目录的路径名,以下是测试:
可见当取消/home的执行权限后,用户test1使用ls命令搜索家目录时提示权限不足。
以上常量的组合:
目录(其中至少含.
和..
)和符号链接(其中含路径,路径长度不为0)的大小永远不为0。
chroot函数可改变进程的根目录,可用于改变根目录以阻止用户访问此目录之外的文件。该函数只能由root用户执行,只要改变了进程的根,该进程和其后代进程就再也不能恢复至原先的根。
finger命令可输出邮件的收到时间和上次读邮件的时间,该命令利用stat函数,邮件被修改的时间即邮件的收到时间,邮件的上次访问时间为上次读邮件的时间。这隐含了当文件以只写打开修改时,文件的访问时间不变,以下是测试过程:
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
struct stat s;
int fd = 0;
if ((fd = open("a.text", O_CREAT | O_EXCL | O_RDWR)) == -1) {
printf("open error\n");
exit(1);
}
if (fstat(fd, &s) == -1) {
printf("fstat error\n");
exit(1);
}
printf("file access time: %ds\n", s.st_atime);
printf("file modify time: %ds\n", s.st_mtime);
close(fd);
sleep(5);
if ((fd = open("a.text", O_WRONLY)) == -1) {
printf("open error\n");
exit(1);
}
if (write(fd, "aa", 2) != 2) {
printf("write error\n");
exit(1);
}
if (fstat(fd, &s) == -1) {
printf("fstat error\n");
exit(1);
}
printf("file access time: %ds\n", s.st_atime);
printf("file modify time: %ds\n", s.st_mtime);
close(fd);
}
运行它:
以上代码修改了文件内容,因此文件的修改时间改变了,如果删去以上代码中向文件写内容的代码,则文件的修改时间不会改变。
内核对目录树的深度没有限制,但如果路径名长度超过PATH_MAX限制时,会有很多命令失败。不是所有平台上都能通过getcwd函数获取长路径名,能获取的需要多次调用realloc得到足够大的缓冲区。cpio不能归档此长路径目录,但tar可以,但tar在Linux 3.2.0上不能解压出目录的层次结构。
创建深目录树:
#include <string.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#define DEPTH 1000
#define STARTDIR "/tmp"
#define NAME "alonglonglonglonglonglonglonglonglonglonglongname"
#define MAXSZ (10*8192)
#define DIR_MODE 0777
#define FILE_MODE 0766
#ifdef PATH_MAX
static long pathmax = PATH_MAX;
#else
static long pathmax = 0;
#endif
static long posix_version = 0;
static long xsi_version = 0;
#define PATH_MAX_GUESS 1024 // if PATH_MAX is indeterminate, no guarantee this is adequate
char *path_alloc(size_t *sizep) {
char *ptr;
size_t size;
if (posix_version == 0) {
posix_version = sysconf(_SC_VERSION);
}
if (xsi_version == 0) {
xsi_version = sysconf(_SC_XOPEN_VERSION);
}
if (pathmax == 0) { // first time through
errno = 0;
if ((pathmax = pathconf("/", _PC_PATH_MAX)) < 0) { // 获取相对于根目录的最长路径长度
if (errno == 0) {
pathmax = PATH_MAX_GUESS;
} else {
printf("pathconf error for _PC_PATH_MAX");
}
} else {
++pathmax; // add one since it's relative to root
}
}
// Before POSIX.1-2001, we aren't guaranteed that PATH_MAX includes the terminating null byte.
// Same goes for XPG3
if ((posix_version < 200112L) && (xsi_version < 4)) {
size = pathmax + 1;
} else {
size = pathmax;
}
if ((ptr = malloc(size)) == NULL) {
printf("malloc error for pathname\n");
exit(1);
}
if (sizep != NULL) {
*sizep = size;
}
return ptr;
}
int main() {
int i;
size_t size;
char *path;
if (chdir(STARTDIR) < 0) {
printf("chdir error\n");
exit(1);
}
for (i = 0; i < DEPTH; ++i) {
if (mkdir(NAME, DIR_MODE) < 0) {
printf("mkdir failed\n");
exit(1);
}
if (chdir(NAME) < 0) {
printf("chdir failed\n");
exit(1);
}
}
if (creat("afile", FILE_MODE) < 0) {
printf("creat error\n");
exit(1);
}
path = path_alloc(&size);
for( ; ; ) {
if (getcwd(path, size) != NULL) {
break;
} else {
printf("getcwd failed, size = %ld\n", (long)size);
size += 100;
if (size > MAXSZ) {
printf("giving up\n");
exit(1);
}
if ((path = realloc(path, size)) == NULL) {
printf("realloc error\n");
exit(1);
}
}
}
printf("length = %ld\n%s\n", (long)strlen(path), path);
exit(0);
}
/dev目录关闭了一般用户的写权限,防止普通用户删除其中的文件,unlink函数也会失败。
上文中的myftw程序从不改变其目录,将其改为每遇到一个目录就用其调用chdir,这样每次调用lstat时就可以使用文件名而非路径名,处理完所有目录项后调用chdir("..")
,比较这种方法和上文中的方法的程序运行时间:
#include <dirent.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <unistd.h>
#include <sys/time.h>
typedef int Myfunc(const char *, const struct stat *, int);
static Myfunc myfunc;
static int myftw(char *, Myfunc *);
static int dopath(Myfunc *);
static long nreg, ndir, nblk, nchr, nfifo, nslink, nsock, ntot;
#ifdef PATH_MAX
static long pathmax = PATH_MAX;
#else
static long pathmax = 0;
#endif
static long posix_version = 0;
static long xsi_version = 0;
#define PATH_MAX_GUESS 1024 // if PATH_MAX is indeterminate, no guarantee this is adequate
char *path_alloc(size_t *sizep) {
char *ptr;
size_t size;
if (posix_version == 0) {
posix_version = sysconf(_SC_VERSION);
}
if (xsi_version == 0) {
xsi_version = sysconf(_SC_XOPEN_VERSION);
}
if (pathmax == 0) { // first time through
errno = 0;
if ((pathmax = pathconf("/", _PC_PATH_MAX)) < 0) {
if (errno == 0) {
pathmax = PATH_MAX_GUESS;
} else {
printf("pathconf error for _PC_PATH_MAX");
}
} else {
++pathmax; // add one since it's relative to root
}
}
// Before POSIX.1-2001, we aren't guaranteed that PATH_MAX includes the terminating null byte.
// Same goes for XPG3
if ((posix_version < 200112L) && (xsi_version < 4)) {
size = pathmax + 1;
} else {
size = pathmax;
}
if ((ptr = malloc(size)) == NULL) {
printf("malloc error for pathname\n");
exit(1);
}
if (sizep != NULL) {
*sizep = size;
}
return ptr;
}
int main(int argc, char *argv[]) {
struct timeval tvStart;
gettimeofday(&tvStart, NULL);
int ret;
if (argc != 2) {
printf("usage: ftw <starting-pathname>\n");
exit(1);
}
ret = myftw(argv[1], myfunc);
ntot = nreg + ndir + nblk + nfifo + nslink + nsock;
if (ntot == 0) { // avoid divid by 0
ntot = 1;
printf("nton is 0\n");
}
printf("regular files = %7ld, %5.2f %%\n", nreg, nreg * 100.0 / ntot);
printf("directories = %7ld, %5.2f %%\n", ndir, ndir * 100.0 / ntot);
printf("block special = %7ld, %5.2f %%\n", nblk, nblk * 100.0 / ntot);
printf("char special = %7ld, %5.2f %%\n", nchr, nchr * 100.0 / ntot);
printf("FIFOs = %7ld, %5.2f %%\n", nfifo, nfifo * 100.0 / ntot);
printf("symbolic links = %7ld, %5.2f %%\n", nslink, nslink * 100.0 / ntot);
printf("sockets = %7ld, %5.2f %%\n", nsock, nsock * 100.0 / ntot);
struct timeval tvEnd;
gettimeofday(&tvEnd, NULL);
printf("use %dms.\n", (tvEnd.tv_sec - tvStart.tv_sec) * 1000 + (tvEnd.tv_usec - tvStart.tv_usec) / 1000);
exit(ret);
}
#define FTW_F 1 // file other than directory
#define FTW_D 2 // directory
#define FTW_DNR 3 // directory that cat't be read
#define FTW_NS 4 // file that we cat't stat
static char *fullpath;
static size_t pathlen;
static int myftw(char *pathname, Myfunc *func) {
fullpath = path_alloc(&pathlen); // just assign a length to pathlen, don't allocate memory
if (pathlen <= strlen(pathname)) {
pathlen = strlen(pathname) * 2;
if ((fullpath = realloc(fullpath, pathlen)) == NULL) {
printf("realloc failed\n");
exit(1);
}
}
strcpy(fullpath, pathname);
return dopath(func);
}
static int dopath(Myfunc *func) {
struct stat statbuf;
struct dirent *dirp;
DIR *dp;
int ret, n;
if (lstat(fullpath, &statbuf) < 0) { // stat error
return func(fullpath, &statbuf, FTW_NS);
}
if (S_ISDIR(statbuf.st_mode) == 0) { // not a directory
return func(fullpath, &statbuf, FTW_F);
}
// now it's a directory
if ((ret = func(fullpath, &statbuf, FTW_D)) != 0) {
return ret;
}
if ((dp = opendir(fullpath)) == NULL) { // can't read directory
return func(fullpath, &statbuf, FTW_DNR);
}
chdir(fullpath);
while ((dirp = readdir(dp)) != NULL) {
if (strcmp(dirp->d_name, ".") == 0 || strcmp(dirp->d_name, "..") == 0) { // ignore dot and dot-dot
continue;
}
strcpy(fullpath, dirp->d_name);
if ((ret = dopath(func)) != 0) { // recursive, deep-first search
break; // time to leave
}
}
chdir("..");
if (closedir(dp) < 0) {
printf("can't close directory %s\n", fullpath);
}
return ret;
}
static int myfunc(const char *pathname, const struct stat *statptr, int type) {
switch (type) {
case FTW_F:
switch (statptr->st_mode & S_IFMT) { // macro defined in sys/stat.h
case S_IFREG: nreg++; break;
case S_IFBLK: nblk++; break;
case S_IFCHR: nchr++; break;
case S_IFIFO: nfifo++; break;
case S_IFLNK: nslink++; break;
case S_IFSOCK: nsock++; break;
case S_IFDIR:
printf("for S_IFDIR for %s\n", pathname);
exit(1);
}
break;
case FTW_D:
++ndir;
break;
case FTW_DNR:
printf("can't read directory %s\n", pathname);
return 0;
break;
case FTW_NS:
printf("stat error for %s\n", pathname);
return 0;
break;
default:
printf("unknown type %d for pathname %s\n", type, pathname);
}
return 0;
}
运行它:
而原版不改变目录的myftw版本运行时间为:
复制带空洞的文件,但复制后将空洞删除,以下是创建带空洞的文件的程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#define MAXBUFSIZE 1024
void mkFileWithHole() {
int fd = 0;
if ((fd = open("fileWithHole", O_CREAT | O_EXCL | O_RDWR)) == -1) {
printf("open error\n");
exit(1);
}
if (write(fd, "aaa", 3) != 3) {
printf("write error\n");
exit(1);
}
if (lseek(fd, 10, SEEK_SET) == -1) {
printf("lseek error\n");
exit(1);
}
if (write(fd, "bbb", 3) != 3) {
printf("write error\n");
exit(1);
}
if (close(fd) == -1) {
printf("close error\n");
exit(1);
}
}
int main() {
mkFileWithHole();
}
运行它:
复制这个文件:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define BUFFSIZE 1024
int main() {
char buf[BUFFSIZE];
int fd = 0;
int readBytes = 0;
if ((fd = open("fileWithHole", O_RDONLY)) == -1) {
printf("open error\n");
exit(1);
}
FILE *fp;
if ((fp = fopen("cpRes", "w")) == NULL) {
printf("fopen error\n");
exit(1);
}
do {
readBytes = read(fd, buf, BUFFSIZE);
if (readBytes == -1) {
printf("read error\n");
exit(1);
}
int nonZeroNum = 0;
char res[BUFFSIZE + 1];
for (int i = 0; i < readBytes; ++i) {
if (buf[i] == '\0') {
continue;
}
res[nonZeroNum] = buf[i];
++nonZeroNum;
}
res[nonZeroNum] = '\0';
fputs(res, fp);
} while (readBytes == BUFFSIZE);
close(fd);
fclose(fp);
exit(0);
}
运行它: