Unix环境高级编程-第四章
1.引言
上一章我们说明了执行IO操作的基本函数,其中的讨论是围绕普通文件IO进行的-打开文件,读文件或写文件。本章将描述文件系统的其他特征和文件的性质。我们将从stat函数开始,逐个说明stat结构的每一个成员以了解文件的所有属性。在此过程中,我们将说明修改这些属性的各个函数(更改所有者,更改权限等),还将更详细地说明UNIX文件系统的结构以及符号链接。本章最后介绍对目录进行操作的各个函数,并且开发了一个以降序遍历目录层次结构的函数。
2.函数stat,fstat,fstatat和lstat
本章主要讨论4个stat函数以及它们的返回信息。
#include<sys/stat.h>
int stat(const char *restrict pathname,struct stat *restrict buf);
int fstat(int fd,struct stat *buf);
int lstat(const char * restrict pathname,struct stat * restrict buf);
int fstatat(int fd,const char *restrict pathname,struct stat *restrict buf,int flag);
一旦给出pathname,stat函数将返回与此命名文件有关的信息结构。fstat函数获得已在描述符fd上打开文件的有关信息。lstat函数类似于stat,但是当命名的文件是一个符号链接时,lstat返回该符号链接的有关信息,而不是由该符号链接引用的文件的信息。
fstatat函数为一个相对于当前打开目录(由fd参数指向)的路径名返回文件统计信息。flag参数控制着是否跟随着一个符号链接。当AT_SYMLINK_NOFOLLOW标志被设置时,fstatat不会跟随符号链接,而是返回符号链接本身的信息。否则,在默认情况下,返回的是符号链接所指向的实际文件的信息。如果fd参数的值是AT_FDCWD,并且pathname参数是一个相对路径名,fstatat会计算相对于当前目录的pathname参数。如果pathname是一个绝对路径,fd参数就会被忽略。这两种情况下,根据flag的取值,fstatat的作用就跟stat或lstat一样。
第2个参数buf是一个指针,它指向一个我们必须提供的结构。函数来填充由buf指向的结构。结构的实际定义可能随具体实现有所不同,但基本形式是:
struct stat{mode_t st_mode; /* file type & mode (permissions) */ino_t st_ino; /* i-node number (serial number) */dev_t st_dev; /* device number (file system) */dev_t st_rdev; /* device number for special files */nlink_t st_nlink; /* number of links */uid_t st_uid; /* user ID of owner*/gid_t st_gid; /* group ID of owner*/off_t st_size; /* size in bytes,for regular files*/struct timespec st_atime; /* time of last access */struct timespec st_mtime; /* time of last modification */struct timespec st_ctime; /* time of last file status change*/blksize_t st_blksize; /* best I/O block size*/blkcnt_t st_blocks; /* number of disk blocks allocated*/
}
timespec结构类型按照秒和纳秒定义了时间,至少包括下面字段:
time_t tv_sec;
long tv_nsec;
注意,stat结构中的大多数成员都是基本系统数据类型。我们将说明此结构的每个成员以了解文件属性。
使用stat函数最多的地方可能就是ls -l 命令,用其可以获得有关一个文件的所有信息。
3.文件类型
至此我们已经介绍了两种不同的文件类型:普通文件和目录。UNIX系统的大多数文件是普通文件或目录,但是也有另外一些类型。文件类型包括如下几种:
(1)普通文件(regular file)。这是最常用的文件类型,这种文件包含了某种形式的数据。至于这种数据是文本还是二进制数据,对于UNIX内核而言并不区别。对普通文件内容的解释由处理该文件的应用程序进行。
一个值得注意的例外是二进制可执行文件。为了执行程序,内核必须理解其格式。所有二进制可执行文件都遵循一种标准化的格式,这种格式使内核能够确定程序文本和数据的加载位置。
(2)目录文件(directory file)。这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件。进程必须使用本章介绍的函数才能更改目录。
(3)块特殊文件(block special file)。这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。
(4)字符特殊文件(character special file)。这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变。系统中所有设备要么是字符特殊文件,要么是块特殊文件。
(5)FIFO。这种类型的文件用于进程间通信,有时也称为命名管道。
(6)套接字。这种类型的文件用于进程间的网络通信。套接字可用于在一台宿主机上进程之间的非网络通信。
(7)符号链接(symbolic link)。这种类型的文件指向另一个文件。
文件类型信息包含在stat结构的st_mode成员中。可以用图4-1中的宏确定文件类型。这些宏的参数都是stat结构中的st_mode成员。
POSIX.1允许实现将进程间通信(IPC)对象(如消息队列和信号量等)说明为文件。图中4-2中的宏可用来从stat结构中确定IPC对象的类型。这些宏与图4-1中的不同,它们的参数并非st_mode,而是指向stat结构的指针。
POSIX.1允许实现将进程间通信(IPC)对象(如消息队列和信号量等)说明为文件。图中4-2中的宏可用来从stat结构中确定IPC对象的类型。这些宏与图4-1中的不同,它们的参数并非st_mode,而是指向stat结构的指针。
实例4-3程序取其命令行参数,然后针对每一个命令行参数打印其文件类型。
#include "apue.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){err_ret("lstat error");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="character 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";elseptr="** unknown mode**";printf("%s\n",ptr);}exit(0);
}
我们特地使用lstat函数一遍检测符号链接。
早期UNIX版本并不提供S_ISxxx宏,于是就需要将st_mode与屏蔽字S_IFMT进行逻辑"与"运算,然后与名为S_IFxxx的常量相比较。大多数系统在文件<sys/stat.h>中定义了此屏蔽字和相关的常量。如若查看此文件,则可找到S_ISDIR宏定义为:
#define S_ISDIR (mode) (((mode)&S_IFMT)==S_ISDIR)
我们说过,普通文件是最主要的文件类型,但是观察以下在一个给定的系统中各种文件的比例是很有意思的。图4-4显示了在一个单用户工作站Linux系统中的统计值和百分比。这些数据是由4.22节中的程序得到的。
4.设置用户ID和设置组ID
与一个进程相关联的ID有6个或更多,如图4-5所示。
(1)实际用户ID和实际组ID标识我们究竟是谁。这两个字段在登陆时取自口令文件中的登录项。通常,在一个登录会话期间这些值并不改变,但是超级用户进程有方法改变它们。
(2)有效用户ID,有效组ID以及附属组ID决定了我们的文件访问权限,下一节对此进行说明。
(3)保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有效用户ID和有效组ID的副本,在8.11节中说明setuid函数时,将说明这两个保存值的作用。
通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID。
每个文件有一个所有者和组所有者,所有者由stat结构中的st_uid指定,组所有者则由st_gid指定。
当执行一个程序文件时,进程的有效用户ID通常就是实际用户ID,有效组ID通常是实际组ID。但是可以在文件模式字(st_mode)中设置一个特殊标志,其含义是"当执行此文件时,将进程的有效用户ID设置为文件所有者的用户ID(st_uid)"。与此相类似,在文件模式字中可以设置另一位,它将执行此文件的进程的有效组ID设置为文件的组所有者ID。在文件模式字中的这两位被称为设置用户iD位和设置组ID位。
5.文件访问权限:
st_mode值也包含了对文件的访问权限位。当提及文件时,指的是前面所提到的任何类型的文件。所有文件类型都有访问权限。很多人认为只有普通文件有访问权限,这是一种误解。
每个文件有9个访问权限位可将它们分成3类,如图4-6.
在图中前3行中。术语用户指的是文件所有者。chmod命令用于修改这9个权限位。该命令允许我们用u表示用户(所有者),用g表示组用o表示其他。有些书把这3种用户类型分别称为所有者,组和世界。这回造成混乱,因为chmod命令用o表示其他,而不是所有者。我们将使用术语用户,组和其他,以便于chmod命令保持一致。
图4-6的3类访问权限以及各种方式由不同的函数使用。我们将这些不同的使用方式汇总在下面。
(1)第一个规则是,我们用名字打开任一类型的文件时,对改名字种包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。这就是为什么对于目录其执行权限位常被称为搜索位的原因。
(2)例如,为了打开文件/usr/include/stdio.h,需要对目录/,/usr和/usr/include具有执行权限。然后,需要具有对文件本身的适当权限,这取决于以何种模式打开它(只读,读-写等)
(3)对于一个文件读权限决定了我们是否能够打开现有文件进行读操作。这与open函数的O_RDONLY和O_RDWR标志相关。
(4)对于一个文件的写权限决定了我们是否能够打开现有的文件进行写操作。这与open函数的O_WRONLY和O_RDWR标志相关。
(5)为了在open函数中对一个文件执行O_TRUNC标志,必须对该文件具有写权限。
(6)为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。对该文件本身则不需要有读,写权限。
(7)如果用7个exec函数中的任一个执行某个文件,都必须有对该文件的执行权限。
进程每次打开,创建或删除一个文件时。内核就进行文件访问权限测试,而这种测试可能涉及文件的所有者(st_uid和st_gid),进程的有效ID以及进程的附属组ID。两个所有者ID是文件的性质,而两个有效ID和附属组ID则是进程的性质。内核进行的测试具体如下。
(1)若进程的有效用户ID是0(超级用户),则允许访问。这给予了超级用户对整个文件系统进行处理的最充分的自由。
(2)若进程的有效用户ID等于文件的所有者ID(也就是进程拥有此文件),那么如果所有者适当的访问权限位被设置,则允许访问;否则拒绝访问。适当的访问权限位指的是,若进程为读而打开该文件,则用户读为应为1;若进程为写而打开该文件,则用户写位应为1:若进程将执行该文件,则用户执行位应为1.
(3)若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果组适当的访问权限位被设置,则允许访问;否则拒绝访问。
(4)若其他用户适当的访问权限位被设置,则允许访问;否则拒绝访问。
按顺序执行这4步,注意,如果进程拥有此文件,则按用户访问权限批准或拒绝该进程对文件的访问–不查看组访问权限。类似地,若进程并不拥有该文件。但进程属于某个适当的组,则按组访问权限批准或决绝该进程对文件的访问–不查看其他用户的访问权限。
6.新文件和目录的所有权
在第3章中讲述用open或creat创建新文件时,我们并没有说明赋予新文件的用户ID和组ID是什么。4.21节将说明mkdir函数,此时就会了解如何创建一个新目录。关于新目录的所有权规则与本节将说明的新文件所有权规则相同。
新文件的用户ID设置为进程的有效用户ID。关于组ID,POSIX.1允许实现选择下列之一作为新文件的组ID。
(1)新文件的组ID可以是进程的有效组ID
(2)新文件的组ID可以是它所在目录的组ID
使用POSIX.1所允许的第二个选项使得在某个目录下创建的文件和目录都具有该目录的组ID。于是文件和目录的组所有权从该点向下传递。例如,在Linux的/var/mail目录中就使用了这种方法。
7.函数access和faccessat
正如前面所说,当用open函数打开一个文件时,内核以进程的有效用户ID和有效组ID为基础执行其访问权限测试。有时,进程也希望按其实际用户ID和实际组ID来测试其访问能力。例如,当一个进程使用设置用户ID或设置组ID功能作为另一个用户运行时,就可能会有这种需要。即使一个进程可能已经通过设置用户ID以超级用户权限运行,它仍可能想验证其实际用户能否访问一个给定的文件。access和faccessat函数是按实际用户ID和实际组ID进行访问权限测试的。
#include<unistd.h>
int access(const char *pathname,int mode);
int faccessat(int fd,const char *pathname,int mode,int flag);
其中,如果测试文件是否已经存在,mode就为F_OK;否则mode是图4-7中的所列常量的按位或。
faccessat函数与access函数在下面两种情况是相同的:一种是pathname参数为绝对路径,另一种fd参数取值为AT_FDCWD而pathname参数为相对路径。否则,faccessat计算相对于打开目录的pathname。
flag参数可以用于改变faccessat的行为,如果flag设置为AT_EACCESS,访问检查用的调用进程的有效用户ID和有效组ID,而不是实际用户ID和实际组ID。
实例4-8:
显示了access函数的使用方法
#include "apue.h"
#include <fcntl.h>int
main(int argc,char *argv[]){if(argc!=2)err_quit("usage:a.out <pathname>");if(access(argv[1],R_OK)<0)err_ret("access error for %s",argv[1]);elseprintf("read access OK\n");if(open(argv[1],O_RDONLY)<0)err_ret("open error for %s",argv[1]);elseprintf("open for reading OK\n");exit(0);
}
8.函数umask
至此我们已说明了与每个文件相关联的9个访问权限位,在此基础上我们可以说明与每个进程相关联的文件模式创建屏蔽字。
umask函数为进程设置文件模式创建屏蔽字,并返回之前的值。
#include<sys/stat.h>
mode_t umask(mode_t cmask);
其中,参数cmask是由图4-6中列出的9个常量(S_IRSR,S_IWUSR等)中的若干个按位或构成的。
在进程创建一个新文件或新目录时,就一定会使用文件模式创建屏蔽字。open和create两个函数都有一个参数mode,它指定了新文件的访问权限位,而在文件模式创建屏蔽字中为1的位,在文件mode中的相应位一定被关闭。
实例4-9:
创建两个文件,创建第一个时,umask值为0,创建第二个时,umask值禁止所有组和其他用户的访问权限。
#include<apue.h>
#include<fcntl.h>#define RWRWRW(S_IRUSR|S_IWUSR|S_IRGRP|S_S_IWGRP|S_IROTH|S_IWOTH)int
main(void)
{umask(0);if(create("foo",RWRWRW)<0)err_sys("create error for foo");umask(S_IrGRP|S_IWGRP|S_IROTH|S_IWOTH);if(create("bar",RWRWRW)<0)err_sys("create error for bar");exit(0);
}
UNIX系统的大多数用户从不处理他们的umask值。通常在登陆时,由shell的启动文件设置一次,然后,再不改变,尽管如此,当编写创建新文件的程序时,如果我们想确保指定的访问权限位已经激活,那么必须在进程运行时修改umask值。例如,如果我们想确保任何用户都能读文件,则应将umask设置为0,否则,我们的进程运行时,有效的umask值可能关闭该权限位。
在前面的实例中,我们用shell的umask命令在运行程序的前后打印文件模式创建屏蔽字。从中可见,更改进程的文件模式创建屏蔽字并不影响其父进程(通常是shell)的屏蔽字。所有shell都有内置umask命令,我们可以用该命令设置或打印当前文件模式创建屏蔽字。
用户可以设置umask值以控制他们所创建文件的默认权限。该值表示成八进制,一位代表一种要屏蔽的权限,这示于图4-10中。设置了相应位后,它所对应的权限就会被拒绝。常用的几种umask值是002,022和027。002阻止其他用户写入你的文件,022阻止同组成员和其他用户写入你的文件,027阻止同组成员写你的文件以及其他用户读写或执行你的文件。
Single UNIX Specification要求shell应该支持符号形式的umask命令。于八进制格式不同,符号格式指定许可的权限而非拒绝的权限
9.函数chmod,fchmod和fchmodat
chmod,fchmod和fchmodat这3个函数使得我们可以更改现有文件的访问权限。
#include<sys/stat.h>
int chmod(const char *pathname,mode_t mode);
int fchmod(int fd,mode_t mode);
int fchmodat(int fd,const char *pathname,mode_t mode.int flag);
chmod函数在指定的文件上进行操作,而fchmod函数则对已经打开的文件进行操作。fchmodat函数于chmod函数在下面两种情况是相同的:一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。否则,fchmodat计算相对于打开目录(由fd参数指向)的pathname。flag参数可以用于改变fchmodat的行为,当设置了AT_SYMLINK_NOFOLLOW标志时,fchmodat并不会跟随符号链接。
为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限。
参数mode时图中4-11中所示常量的按位或。
10.粘着位
S_ISVTX位有一段有趣的历史。在UNIX尚未使用请求分页式技术的早期版本中,S_ISVTX位被称为粘着位。如果一个可执行程序文件的这一位被设置了,那么当该程序第一次被执行,在其终止时,程序正文部分的一个副本仍被保存在交换区(程序的正文部分时机器指令)。这使得下次执行该程序时能较快地将其装入内存。其原因是:通常的UNIX文件系统中,文件的各个数据块很可能是随机存放的,相比较而言,交换区是被作为一个连续文件来处理的。对于通用的应用程序,如文本编辑器合C语言编辑器,我们通常设置他们所在文件的粘着位。自然的,对于在交换区中可以同时存放的设置了粘着位的文件数是有限制的,以免过多占用交换区空间,但无论如何这是一个有用的技术。因为在系统再次自举前,文件的正文部分总是在交换区中,这正是名字中“粘着位”的由来。后来的UNIX版本称它位保存正文位,因此也就有了常量S_ISVTX。现今较新的UNIX系统大多数都配置了虚拟存储系统以及快速文件系统,所以不再需要使用这种技术。
现今的系统扩展了粘着位的使用范围,Single UNIX Specification允许针对目录设置粘着位。如果对一个目录设置了粘着位,只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录下的文件:
(1)拥有此文件;
(2)拥有此目录;
(3)是超级用户;
目录/tmp合/var/tmp是设置了粘着位的典型候选者–任何用户都可以在这两个目录中创建文件,任一用户对这两个目录的权限通常都是读,写合执行。但是用户不应能删除或重命名属于其他人的文件,为此在这两个目录的文件模式中都设置了粘着位。
11.函数chown,fchown,fchownat合lchown
下面几个chown函数可用于更改文件的用户ID和组ID。如果两个参数owner或group中的任意一个是-1,则对应的ID不变。
#include<unistd.h>
int chown(const char *pathname,uid_t owner,gid_t group);
int fchown(int fd,uid_t owner,gid_t group);
int lchown(const char *pathname,uid_t owner,gid_t group);
(1)除了所引用的文件是符号链接以外,这4个函数的操作类似。在符号链接情况下,lchown和fchownat(设置了AT_SYMLINK_NOFOLLOW标志)更改符号链接本身的所有者,而不是该符号链接所指向的文件的所有者。
(2)fchown函数改变fd参数指向的打开文件的所有者,既然它在一个已打开的文件上操作,就不能用于改变符号链接的所有者。
fchownat函数与chown或者lchown函数在下面两种情况下是相同的:
一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。在这两种情况下,如果flag参数中设置了AT_SYMLINK_NOFOLLOW标志,fchownat与lchown行为相同,如果flag参数中清除了AT_SYMLINK_NOFOLLOW标志,则fchownat与chown行为相同。如果fd参数设置为打开目录的文件描述符,并且pathname参数是一个相对路径名,fchownat函数计算相对于打开目录的pathname。
(3)基于BSD的系统一直规定只有超级用户才能更改一个文件的所有者,这样做的原因是防止用户改变其文件的所有者从而摆脱磁盘空间限制限额对他们的限制。System V则允许任一用户更改他们所拥有的文件的所有者。
(4)_POSIX_CHOWN_RESTRICTED常量可选地定义在头文件<unistd.h>中,而且总是可以用pathconf或fpathconf函数进行查询。此选项还与所引用的文件有关。
若_POSIX_CHOWN_RESTRICTED对指定的文件生效,则
①只有超级用户进程能更改该文件的用户ID
②如果进程拥有此文件,参数owner等于-1或文件的用户ID,并且参数group等于进程的有效组ID或进程的附属组ID之一,那么一个非超级用户进程可以更改该文件的组ID。
这意味着,当_POSIX_CHOWN_RESTRICTED有效时,不能更改其他用户文件的用户ID。你可以更改你所用的文件的组ID,但只能改到你所属的组。
12.文件长度
stat结构成员st_size表示以字节为单位的长度。此字段只对普通文件,目录文件和符号链接有意义。
对于普通文件,其文件长度可以是0,在开始读这种文件时,将得到文件结束指示。对于目录,文件长度通常是一个数的整倍数,我们将在22节中说明读目录操作。
对于符号链接,文件长度是在文件名中的实际字节数。例如,文件长度7就是路径名/usr/lib的长度
现今,大多数现代的UNIX系统提供字段st_blksize和st_blocks。其中第一个是对文件IO较合适的块长度,第二个是所分配的实际512字节块块数。
13.文件截断
有时我们需要在文件尾端截取一些数据以缩短文件。将一个文件的长度截断为0是一个特例,在打开文件时用O_TRUNC标志可以做到这一点。为了截断文件可以调用函数truncate和ftruncate。
#include<unistd.h>
int truncate(const char*pathname,off_t length);
int ftruncate(int fd,off_t length);
这两个函数将一个现有文件长度截断为length。如果该文件以前长度大于length,则超过length以外的数据就不能访问。如果以前的长度小于length,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0
14.文件系统
为了说明文件链接的概念,先要介绍UNIX文件系统的基本结构。同时,了解i节点和指向i节点的目录项之间的区别也是很有益的。
目前,正在使用的UNIX文件系统有多种实现。例如,Solaris支持多种不同类型的磁盘文件系统:传统的基于BSD的UNIX文件系统(称为UFS),读,写DOS格式软盘的文件系统(称为PCFS),以及读CD的文件系统(称为HSFS)。在图2-20中,我们已经看到了不同类型系统的一个区别。UFS是以Berkeley快速文件系统为基础的。
我们可以把一个磁盘分成一个或多个分区,每个分区可以包含一个文件系统,i节点是固定长度的记录项,它包含有关文件的大部分信息。
如果更仔细地观察一个柱面组的i节点和数据块部分,则可以看到图4-14中所示的情况。
图中有两个目录项指向同一个i节点。每个i节点中都有一个链接计数,其值就是指向该i节点的目录项数。只有当链接计数减少至0时,才可以删除该文件。这就是为什么删除一个文件的链接并不总是意味着释放该文件占用的磁盘块的原因。这也是为什么删除一个目录项的函数被称之为unlink而不是delete的原因。在stat结构中,链接计数包含在st_nlink成员中,其基本系统数据类型时nlink_t。这种链接类型称之为硬链接。在POSIX.1中,常量LINK_MAX指定了一个文件连接数的最大值。
另一种链接类型称为符号链接。符号链接文件的实际内容包含了该符号所指向的文件的名字。
i节点包含了文件有关的所有信息:文件类型,文件访问权限位,文件长度和指向文件数据块的指针等。stat结构中的大多数信息都取值i节点。只有两项重要数据存放在目录项中:文件名和i节点编号。i节点编号的数据类型ino_t。
因为目录项中的i节点编号指向同一文件系统中的相应i节点,一个目录项不能指向另一个文件系统的i节点。
当在不更换文件系统的情况下为一个文件重命名时,该文件的实际内容并未移动,只需要构造一个指向现有i节点的新目录项,并删除老的目录项。链接计数不会改变。
15.函数link,linkat,unlink,unlinkat和remove
如上节所述,任何一个文件可以有多个目录项指向其i节点。创建一个指向现有文件的链接的方法时使用link函数或linkat函数。
#include<unistd.h>
int link(const char *existingpath,const char *newpath);
int linkat(int efd,const char *existingpath,int nfd,const char *newpath,int flag);
这两个函数创建一个新目录想newpath,它引用现有文件existingpath。如果newpath已经存在,则返回出错。只创建newpath中的最后一个分量,路径中的其他部分应当已经存在。
对于linkat函数,现有文件是通过efd和existingpath参数指定的,新的路径名是通过nfd和newpath参数指定的。默认情况下,如果两个路径名中的任一个是相对路径,那么它需要通过相对于对应的文件描述符进行计算。如果两个文件描述符中的任一个设置为AT_FDCWD,那么相应的路径名就通过相对于当前目录进行计算。如果任一路径名是绝对路径,相应的文件描述符参数就会被忽略。
当现有文件是符号链接时,有flag参数来控制linkat函数时创建指向现有符号链接的链接还是创建指向现有符号链接所指向的文件的链接。如果在flag参数中设置了AT_SYMLINK_FOLLOW标志,就创建指向符号链接目标的链接。如果这个标志被清除了,则创建一个指向符号链接本身的链接。
为了删除一个现有的目录项,可以调用unlink函数。
#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd,const char *pathname,int flag);
这两个函数删除目录项,并将由pathname所引起文件的链接计数减1.如果对该文件还有其他链接,则仍可通过其他链接访问该文件的数据。如果出错,则不对该文件做任何更改。
只有当链接计数达到0时,该文件的内容才可被删除。另一个条件也会阻止删除文件的内容–只要由进程打开了该文件,其内容也不能删除。关闭一个文件时,内核首先检查打开该文件的进程个数;如果这个计数达到0,内核再去检查其链接计数;如果计数也是0,那么就删除该文件的内容。
如果pathname参数时相对路径名,那么unlink函数计算相对于由fd文件描述符参数代表的目录的路径名。如果fd参数设置为AT_FDCWD,那么通过相对于调用进程的当前工作目录来计算路径名。如果pathname参数时绝对路径名,那么fd参数被忽略。
flag参数给出了一种方法,使得调用进程可以改变unlinkat函数的默认行为。当AT_REMOVEDIR标志被设置时,unlinkat函数可以类似于rmdir一样删除目录。如果这个标志被清除,unlinkat与unlink执行同样的操作。
实例4-16:
程序打开一个文件,然后解除它的链接,执行该程序的进程然后睡眠15秒,接着就终止
#include "apue.h"
#include <fcntl.h>int
main(void)
{if(open("tempfile",O_RDWR)<0)err_sys("open error");if(unlink("tempfile")<0)err_sys("unlink error");printf("file unlinked\n");sleep(15);printf("done\n");exit(0);
}
unlink这种特性经常被程序用来确保即使是在程序崩溃时,它所创建的临时文件也不会遗留下来。进程用open或create创建一个文件,然后立即调用unlink,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时,该文件的内容才被删除。
如果pathname是符号链接,那么unlink删除该符号链接,而不是该链接所引用的文件。给出符号链接名的情况下,没有一个函数能删除由该链接所引用的文件。如果文件系统支持的化,超级用户可以调用unlink,其参数pathname指定一个目录,但是通常应当使用rmdir函数,而不是用unlink这种方式。
我们也可以使用remove函数解除对一个文件或目录的链接,对于文件,remove的功能与unlink相同,对于目录,remove的功能与rmdir相同。
#include <stdio.h>
int remove(const char*pathname);
16.函数rename和renameat
文件或目录可以用rename函数或者renameat函数进行重命名
#include<stdio.h>
int rename(const char *oldname,const char *newname)
int renameat(int oldfd,const char *oldname,int newfd,const char *newname);
根据oldname是指文件,目录还是符号链接,有几种情况需要加以说明。我们也必须说明如果newname已经存在时将会发生什么。
(1)如果oldname指的是一个文件而不是目录,那么若newname已存在,则其不能是目录,进程会删除该目录项并且oldname重命名为newname
(2)如果oldname指的是一个目录,则newname若存在,必须是引用一个目录,且是空目录,则进程会删除该目录并且将oldname重命名为newname。
(3)如果oldname和newname引用符号链接,则处理的是符号链接本身,而不是它所引用的文件。
(4)不能对.和…重命名
17.符号链接
符号链接是对一个文件的间接指针,它与上一节所述的硬链接有所不同,硬链接指向文件的i节点。引入符号链接的原因是为了避开硬链接的一些限制。
(1)硬链接通常要求链接和文件位于同一文件系统中。
(2)只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)
对符号i按揭以及它指向何种对象并无任何文件系统限制,任何用户都可以创建指向目录的符号链接。符号链接一般用于将一个文件或整个目录结构移到系统中的另一个位置。
18.创建和读取符号链接
可以用symlink或symlinkat函数创建一个符号链接
#include<unistd.h>
int symlink(const char *actualpath,const char *sympath);
int symlinkat(const char *actualpath,int fd,const char *sympath);
函数创建了一个指向actualpath的新目录项sympath。在创建此符号链接时,并不要求actualpath已经存在,并且actualpath和sympath并不需要位于同一文件系统中。
symlinkat函数和symlink函数类似,但sympath参数根据相对于打开文件描述符引用的目录进行计算。如果sympath参数指定的是绝对路径或者fd参数设置了AT_FDCWD值,那么symlinkat就等同于symlink函数。
因为open函数跟随符号链接,所以需要有一种方法打开该链接本身,并读该链接中的名字。readlink和readlinkat函数提供了这种功能。
#include<unistd.h>
ssize_t readlink(const char *restrict pathname,char *restrict buf,size_t bufsize);
ssize_t readlinkat(int fd,const char* restrict pathname,char *restrict buf,size_t bufsize);
两个函数组合了open,read和close的所有操作。如果成功执行,则返回读入buf的字节数。
19.文件的时间
每个文件维护3个时间字段,它们的意义示于图4-19中
注意,修改时间(st_mtim)和状态更改时间(st_ctim)之间的区别。修改时间是文件内容最后一次被修改的时间。状态更改时间是该文件的i节点最后一次被修改的时间。在本章中我们已说明了很多影响到i节点的操作,如更改文件的访问权限,更改用户ID,更改链接数等,但它们并没有更改文件的实际内容。因为i节点中的所有信息都是与文件的实际内容分开存放的,所以除了要记录文件数据修改时间以外,还需要记录状态更改时间,也就是i节点中信息的时间。
图4-20列出了我们已说明过的各种函数对这3个时间的作用。
20.函数futimens,utimensat和utimes
一个文件的访问和修改时间可以用以下几个函数更改。futimens和utimensat函数可以指定纳秒级精度的时间戳。用到的数据结构是与stat函数族相同的timespec结构。
#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 flag);
这两个函数的times数组参数的第一个元素包含访问时间,第二元素包含修改时间。这两个时间值是日历时间,如1.10节所述,这是自特定时间(1970年1月1日00:00:00)以来所经过的秒数,不足秒的部分用纳秒表示。
时间戳可以按下列4中方式之一进行指定。
(1)如果times参数是一个空指针,则访问时间和修改时间两者都设置为当前时间。
(2)如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳就设置为当前时间,忽略相应的tv_sec字段。
(3)如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_OMIT,相应的时间戳保持不变,忽略相应的tv_sec字段。
(4)如果times参数指向两个timespec结构的数组,且tv_nsec字段的值既不是UTIME_NOW也不是UTIME_OMIT,在这种情况下,相应的时间戳设置为相应的tv_sec和tv_nsec字段的值。
执行这些函数所要求的优先权取决于times参数的值。
①如果times是一个空指针,或者任一tv_nsec字段社会UTIME_NOW,则进程的有效用户ID必须等于该文件的所有者ID;进程对该文件必须具有写权限,或者进程是一个超级用户进程。
②如果times是非空指针,并且任一tv_nsec字段的值既不是UTIME_NOW也不是UTIME_OMIT,则进程的有效用户ID必须等于该文件的所有者ID,或者进程必须是一个超级用户进程。对文件只具有写权限是不够的。
③如果times是非空指针,并且两个tv_nsec字段的值都为UTIME_OMIT,就不执行任何的权限检查。
futimens函数需要打开文件来更改它的时间,utimensat函数提供了一种使用文件名更改文件时间的方法。pathname参数是相对于fd参数进程计算的,fd要么是打开目录的文件描述符,要么设置为特殊值AT_FDCWD。如果pathname指定了绝对路径,那么fd参数被忽略。
utimensat的flag参数可用于进一步修改默认行为。如果设置了AT_SYMLINK_NOFOLLOW标志,则符号链接本身的时间就会被修改。默认的行为是跟随符号链接,并把文件的时间改成符号链接的时间。
futimens和utimensat函数都包含在POSIX.1中,第3个函数utimes包含在Single UNIX Specification的XSI扩展选项中。
#include<sys/time.h>
int utimes(const char *pathname,const struct timeval times[2]);
utimes函数对路径名进行操作,times参数是指向包含两个时间戳(访问时间和修改时间)元素的数组的指针,两个时间戳是用秒和微妙表示的。
struct timeval{time_t tv_sec;long tv_usec;
}
注意,我们不能对状态更改时间st_ctim(i节点最近被修改的时间)指定一个值,因为调用utimes函数时,此字段会被自动更新。
在某些UNIX版本中,touch命令使用这些函数中的某一个。另外,标准归档程序tar和cpio可选地调用这些函数,一遍将
实例4-21:
程序使用待O_TRUNC选项的open函数将文件截断为0但并不更改其访问时间及修改时间。为了做到这一点,首先用stat函数得到这些时间,然后截断文件,最后再用futimens函数重置这两个时间。
#include "apue.h"
#include<fcntl.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[1],&statbuf)<0){err_ret("%s:stat error",argv[i]);continue;}if((fd=open(argv[1],O_RDWR|O_TRUNC))<0){err_ret("%s: open error",argv[i]);continue;}times[0]=statbuf.st_atim;times[1]=statbuf.st_mtim;if(futimens(fd,times)<0)err_ret("%s: futimens error",argv[i]);close(fd);}}
21.函数mkdir,mkdirat和rmdir
用mkdir和mkdirat函数创建目录,用rmdir函数删除目录。
#include<sys/stat.h>
int mkdir(const char *pathname,mode_t mode);
int mkdirat(int fd,const char *pathname,mode_t mode);
这两个函数创建一个新的空目录。其中.和…目录项时自动创建的。所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。
常见的错误是指定与文件相同的mode。但是,对于目录通常至少要设置一个执行权限位,以允许访问该目录中的文件名。
mkdirat和mkdir函数类似,当fd参数具有特殊值AT_FDCWD或者pathname参数指定了绝对路径名时,mkdirat与mkdir完全一样。否则,fd参数是一个打开目录,相对路径名根据此打开目录进行计算。
用rmdir函数可以删除一个空目录。空目录只包含.和…这两项的目录。
#include<unistd.h>
int rmdir(const char *pathname);
如果调用此函数使目录的链接计数成为0,并且也没有其他进程打开此目录,则释放由此目录占用的空间。如果在链接计数达到0时,有一个或多个进程打开此目录,则在此函数返回前删除最后一个链接.和…项。另外,在此目录中不能再创建新文件。但是再最后一个进程关闭它之前并不释放此类目录。
22.读目录
对某个目录具有访问权限的任一用户都可以读该目录,但是,为了防止文件系统产生混乱,只有内核才能写目录。一个目录的写权限位和执行权限位决定了在该目录中能否创建新文件以及删除文件,它们并不表示能否写目录本身。
目录的实际格式依赖于UNIX系统实现和文件系统的涉及。
#include<dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
struct dirent *readdir(DIR *dp);
void rewinddir(DIR *dp);
int closedir(DIR *dp);
long telldir(DIR *dp);
void seekdir(DIR *dp,long loc);
fdopendir函数提供了一种方法,可以把打开文件描述符转换成目录处理函数需要的DIR结构。
telldir和seekdir函数不是基本的POSIX.1标准的组成部分。它们是Single UNIX Specification中的XSI扩展,所以可以期望所有符合UNIX系统的实现会提供这两个函数。
定义在头文件<dirent.h>中的dirent结构与实现有关。实现对此结构所做的定义至少包含下列两个成员:
ino_t d_ino;
char d_name[];
注意d_name的大小并没有指定,但必须至少包含NAME_MAX个字节(不包含NULL字节),因为文件名是以null字节结束的,所以在头文件中如何定义数组d_name并无多大关系,数组大小并不表示文件名的长度。
DIR结构是一个内部结构,上述7个函数用这个内部结构保存当前正在被读的目录的有关信息。其作用类似于FILE结构。FILE结构由标准IO库维护,我们将在第5章对它进行说明。
由opendir和fdopendir返回的指向DIR结构的指针由另外5个函数使用。opendir执行初始操作,使第一个readdir返回目录中的第一个目录项。DIR结构由fdopendir创建时,readdir返回的第一项取决于传给fdopendir函数的文件描述符相关联的文件偏移量。
23.函数chdir,fchdir和getcwd
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜线开始的路径名为相对路径名)。当用户登录到UNIX系统时,其当前工作目录通常是口令文件(/etc/passwd)中该用户登录项的第6个字段–用户的起始目录。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。
进程调用chdir或fchdir函数可以更改当前工作目录。
#include<unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
在这两个函数中,分别用pathname或打开文件描述符来指定新的当前工作目录。
实例:
因为当前工作目录是进程的一个属性,所以只影响调用chdir的进程本身,而不影响其它进程,这就意味着图4-23的程序并不会产生我们可能希望得到的结果。
#include "apue.h"
int
main(void)
{if(chdir("/tmp")<0)err_sys("chdir failed");printf("chdir to /tmp/successed\n");exit(0);
}
每个程序运行在独立的进程中,shell的当前工作目录并不会随着程序调用chdir而改变。由此可见,为了改变shell进程自己的工作目录,shell应当直接调用chdir函数,为此cd命令内建在shell中。
因为内核必须维护当前工作目录的信息,所以应能获取其当前值。遗憾的是,内核为每个进程只保存指向该目录v节点的指针等目录本身的信息,并不保存该目录的完整路径名。
我们需要一个函数,它从当前工作目录.开始,用…找到其上一级目录,然后读其目录项,直到该目录项中的i节点编号与工作目录i节点编号相同,这样地就找到了其对应的文件名。按照这种方法,逐层上移,直到遇到根,这样就得到了当前工作目录完整的绝对路径名。很幸运,函数getcwd就提供了这种功能。
#include<unistd.h>
char *getcwd(char *buf,size_t size);
必须向此函数传递两个参数,一个是缓冲区地址buf,另一个是缓冲区的长度size(以字节为单位)。该缓冲区必须有足够的长度以容纳绝对路径名再加上一个终止null字节,否则返回出错。
实例4-22:
程序将工作目录更改至一个指定的目录,然后调用getcwd,最后打印该工作目录。
#include "apue.h"
int
main(void)
{char *ptr;size_t size;if(chdir("/usr/spool/uucpublic")<0)err_sys("chdir failed");ptr=path_alloc(&size);if(getcwd(ptr,size)==NULL)err_sys("getcwd failed");printf("cwd= %s\n",ptr);exit(0);
}
注意,chdir跟随符号链接,但是当getcwd沿着目录数上溯遇到/var/spool目录时,它并不了解该目录由符号/usr/spool所指向。这是符号链接的一种特性。
当一个应用程序需要再文件系统中返回到它工作的出发点时,getcwd函数是有用的。在更换工作目录之前,我们可以调用getcwd函数先将其保存起来。在完成了处理后,就可以将所保存的原工作目录路径名作为调用参数传给chdir,这样就返回了文件系统中的出发点。
fchdir函数向我们提供了一种完成此任务的便捷方法。在更换到文件系统中不同位置前,无需调用getcwd函数,而是使用open打开当前工作目录,然后保存其返回的文件描述符。当希望回到原工作目录时,只要简单地将该文件描述符传送给fchdir。
24.设备特殊文件
st_dev和st_rdev这两个字段经常引起混淆,有关规则:
(1)每个文件系统所在的存储设备都由其主,此设备号表示。设备号所用的数据类型是基本系统数据类型dev_t。主设备号标识设备驱动程序,有时编码为其通信的外设板;次设备号标识特定的子设备。一个磁盘驱动器经常包含若干个文件系统。在同一磁盘驱动器上的各文件系统通常具有相同的主设备号,但是次设备号却不同。
(2)我们通常可以使用两个宏:major和minor来访问主,次设备号,大多数实现都定义这两个宏。这就意味着我们无需关心这两个参数是如何存放在dev_t对象中的。
(3)系统中与每个文件名关联的st_dev值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的i节点。
(4)只有字符特殊文件和快特殊文件才有st_rdev值。此值包含实际设备的设备号。
实例4-25:
程序为每个命令行参数打印设备号,另外若此参数引用的是字符特殊文件或块特殊文件,则还打印该特殊文件的st_rdev值。
#include "apue.h"
#ifdef SOLARIS
#include <sys/mkdev.h>
#endifint
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){err_ret("stat error");}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)) ? "character":"block",major(buf.st_rdev),minor(buf.st_rdev));}printf("\n"); }exit(0);
}
25.文件访问权限位小结
我们已经说明了所有文件访问权限位,其中某些位有多种用途。图4-26列出了所有这些权限位,以及它们对普通文件和目录文件的作用。
最后9各常量还可以分成如下3组:
S_IRWXU=S_IRUSR|S_IWUSR|S_IXUSR
S_IRWXG=S_IRGRP|S_IWGRP|S_IXGRP
S_IRWXO=S_IROTH|S_IWOTH|S_IXOTH
Unix环境高级编程-第四章相关推荐
- UNIX环境高级编程--第七章
1进程终止 进程正常终止: view plaincopy to clipboard #include<stdlib.h> void exit(int status); void ...
- UNIX环境高级编程 第11章 线程
使用C++调用pthread_cleanup_push( )时,下面的代码是无法编译通过的: pthread_cleanup_push(cleanup, "thread 1 first ha ...
- UNIX 环境高级编程(四)—— dirent.h
dirent.h 是 POSIX.1 标准定义的 unix 类目录操作的头文件,包含了许多 UNIX 系统服务的函数原型,例如 opendir 函数.readdir 函数. 1. 基本函数接口 1.1 ...
- UNIX环境高级编程第三章
1.对于内核,所有打开或者新创建的文件都通过文件描述符引用.open或者creat获得的文件描述符传递给read或者write做相关函数的第一参数. [1]open函数,除非创建新的函数带第三个参数, ...
- UNIX环境高级编程 第12章 线程控制
c'nblogs 转载于:https://www.cnblogs.com/pluse/p/6889874.html
- unix环境高级编程基础知识之第二篇(3)
看了unix环境高级编程第三章,把代码也都自己敲了一遍,另主要讲解了一些IO函数,read/write/fseek/fcntl:这里主要是c函数,比较容易,看多了就熟悉了.对fcntl函数讲解比较到位 ...
- 开发日记-20190822 关键词 读书笔记《Unix环境高级编程(第二版)》《掌控习惯》DAY 2
Preface 话说,昨天开始尝试着去改变自己,从基础的习惯开始,11:30准时睡觉,平时差不多12:30才睡觉.按理说,比平时早了一个小时睡觉吧,然后我就把闹钟提前了45分钟,想着还能比平常多睡15 ...
- 《Unix环境高级编程》学习笔记:从点到面
以前在课堂上学习过<Unix初级教程(第四版)>,对于Unix有了一点了解.由于以后使用的需要,要对它进行比较深入的学习,为此需要阅读不少的书籍,这本<Unix环境高级编程>便 ...
- [阅读体会] UNIX环境高级编程
文章目录 写在开始阅读前 (我会的知识点,在体会里是不提的,并不是书中没有,这里着重于记录我之前不会的,但读过书之后学会的.) 第一章 第二章 2.1 引言 2.2 UNIX标准化 2.2.1 ISO ...
最新文章
- Windows10下python-pcl的安装步骤说明,亲测ok
- 树莓派避障小车(python)
- 指定eclipse启动使用的jdk,路径有空格的情况
- 为什么要学习DOS?
- s5 android5.0内存泄漏,android-最初从位图泄漏了未引用的byte [],但被回收的()导致内存泄漏(直到活动停止)...
- shell编程之数学运算
- [architecture]-arm exclusive机制介绍
- “云智一体“全场景智能视频技术与应用解析白皮书下载申请
- 新浪微博客户端(eoe)
- RabbitMQ知多少
- JS-深入理解继承(非class方式与class继承)
- android 滚动条 相关属性
- ps去水印教程_叫板 PS!去水印、抠图、加滤镜,这款超强修图应用到底什么来头...
- 多功能数字时钟(VHDL)
- MSDC 4.3 接口规范(29)
- [OHIF-Viewers]医疗数字阅片-医学影像-cornerstone-core-Cornerstone.js提供了一个完整的基于Web的医学成像平台。...
- AT24C02 能读不能写的问题
- (二)使用npm搭建React项目
- ibm服务器x3650m4引导,IBM X3650 M4安装win 2008 Server操作指南
- 书画拍卖系统 php源码,网上拍卖系统,源代码