APUE读书笔记-第四章 文件和目录

到第四章了,不知什么时候才能把这本书看完,耽误的时间太多了。

第四章是在第三章的基础上,主要描述文件系统的其他性质和文件的性质。

4.2 stat、fstat、fstatat、lstat函数

首先来看看这四个函数的原型:

#include <sys/stat.h> ///usr/include/x86_64-linux-gnu/sys/
int stat (const char *__restrict __file, struct stat *__restrict __buf)
int fstat (int __fd, struct stat *__buf)
int fstatat (int __fd, const char *__restrict __file, struct stat *__restrict __buf, int __flag)
int lstat (const char *__restrict __file, struct stat *__restrict __buf)
这几个函数的功能概括起来就是:获得stat结构体。区别就在于stat函数通过文件名获得这一结构体;fstat通过文件描述符;lstat返回该符号链接的有关信息,而不是由该符号链接引用的文件的信息;fstatat返回当前文件夹下(由fd指向)某个文件(文件名为file)的stat结构体。

好了,不管上面的几个函数具体功能为何,其核心都是返回stat结构体,stat定义如下:

struct stat
  {
    __dev_t st_dev;		/* Device.  */
#ifndef __x86_64__
    unsigned short int __pad1;
#endif
#if defined __x86_64__ || !defined __USE_FILE_OFFSET64
    __ino_t st_ino;		/* File serial number.	*/
#else
    __ino_t __st_ino;			/* 32bit file serial number.	*/
#endif
#ifndef __x86_64__
    __mode_t st_mode;			/* File mode.  */
    __nlink_t st_nlink;			/* Link count.  */
#else
    __nlink_t st_nlink;		/* Link count.  */
    __mode_t st_mode;		/* File mode.  */
#endif
    __uid_t st_uid;		/* User ID of the file's owner.	*/
    __gid_t st_gid;		/* Group ID of the file's group.*/
#ifdef __x86_64__
    int __pad0;
#endif
    __dev_t st_rdev;		/* Device number, if device.  */
#ifndef __x86_64__
    unsigned short int __pad2;
#endif
#if defined __x86_64__ || !defined __USE_FILE_OFFSET64
    __off_t st_size;			/* Size of file, in bytes.  */
#else
    __off64_t st_size;			/* Size of file, in bytes.  */
#endif
    __blksize_t st_blksize;	/* Optimal block size for I/O.  */
#if defined __x86_64__  || !defined __USE_FILE_OFFSET64
    __blkcnt_t st_blocks;		/* Number 512-byte blocks allocated. */
#else
    __blkcnt64_t st_blocks;		/* Number 512-byte blocks allocated. */
#endif
#ifdef __USE_XOPEN2K8
    /* Nanosecond resolution timestamps are stored in a format
       equivalent to 'struct timespec'.  This is the type used
       whenever possible but the Unix namespace rules do not allow the
       identifier 'timespec' to appear in the <sys/stat.h> header.
       Therefore we have to handle the use of this header in strictly
       standard-compliant sources special.  */
    struct timespec st_atim;		/* Time of last access.  */
    struct timespec st_mtim;		/* Time of last modification.  */
    struct timespec st_ctim;		/* Time of last status change.  */
# define st_atime st_atim.tv_sec	/* Backward compatibility.  */
# define st_mtime st_mtim.tv_sec
# define st_ctime st_ctim.tv_sec
#else
    __time_t st_atime;			/* Time of last access.  */
    __syscall_ulong_t st_atimensec;	/* Nscecs of last access.  */
    __time_t st_mtime;			/* Time of last modification.  */
    __syscall_ulong_t st_mtimensec;	/* Nsecs of last modification.  */
    __time_t st_ctime;			/* Time of last status change.  */
    __syscall_ulong_t st_ctimensec;	/* Nsecs of last status change.  */
#endif
#ifdef __x86_64__
    __syscall_slong_t __glibc_reserved[3];
#else
# ifndef __USE_FILE_OFFSET64
    unsigned long int __glibc_reserved4;
    unsigned long int __glibc_reserved5;
# else
    __ino64_t st_ino;			/* File serial number.	*/
# endif
#endif
  };

4.3文件类型

看过了上面的stat结构体,随意感受了一下stat结构体的内容,接下来对其中几个重要的字段进行研究,首先来看:

 __mode_t st_mode;

先来看看“ __mode_t”的类型,相关定义位于:/usr/include/x86_64-linux-gnu/bits/type.h中,具体定义如下:

# define __STD_TYPE		typedef
__STD_TYPE __MODE_T_TYPE __mode_t;
#define __MODE_T_TYPE        __U32_TYPE ///usr/include/x86_64-linux-gnu/bits/typesize.h
#define __U32_TYPE        unsigned int

这一大堆,来回来去的。

根据APUE,文件共包括以下几类:

  1. 普通文件
  2. 目录文件
  3. 块特殊文件
  4. 字符特殊文件
  5. FIFO
  6. 套接字
  7. 符号链接

好了,再来看看linux的有关内容,还是位于/usr/include/x86_64-linux-gnu/sys/stat.h中

#define	__S_ISTYPE(mode, mask)	(((mode) & __S_IFMT) == (mask))

#define	S_ISDIR(mode)	 __S_ISTYPE((mode), __S_IFDIR)
#define	S_ISCHR(mode)	 __S_ISTYPE((mode), __S_IFCHR)
#define	S_ISBLK(mode)	 __S_ISTYPE((mode), __S_IFBLK)
#define	S_ISREG(mode)	 __S_ISTYPE((mode), __S_IFREG)
#ifdef __S_IFIFO
# define S_ISFIFO(mode)	 __S_ISTYPE((mode), __S_IFIFO)
#endif
#ifdef __S_IFLNK
# define S_ISLNK(mode)	 __S_ISTYPE((mode), __S_IFLNK)
#endif
# define S_ISSOCK(mode) __S_ISTYPE((mode), __S_IFSOCK)
#ifdef    __USE_POSIX199309
# define S_TYPEISMQ(buf) __S_TYPEISMQ(buf)
# define S_TYPEISSEM(buf) __S_TYPEISSEM(buf)
# define S_TYPEISSHM(buf) __S_TYPEISSHM(buf)
#endif

好了,通过上面的定义可以了解到这些宏定义说到底就是通过一个参数与__S_IFMT进行“与运算”,得到值后与函数的本来功能进行比较,最后返回0或1。

在上一章中我们留下了一个疑问,通过lseek函数可以设定文件的偏移量,但若文件描述符指向管道、FIFO(命名管道)、网络套接字,则lseek返回-1,并将errno设置为ESPIPE。所以我们得到结论文件描述符0、1、2是管道或命名管道。现在终于可以通过实验验证一下了:

#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
	struct stat buf;
	fstat(STDIN_FILENO,&buf);
	if(S_ISFIFO(buf.st_mode));
		printf("stdin is fifo\n");
	fstat(STDOUT_FILENO,&buf);
	if(S_ISFIFO(buf.st_mode));
		printf("stdout is fifo\n");
	fstat(STDERR_FILENO,&buf);
	if(S_ISFIFO(buf.st_mode));
		printf("stderr is fifo\n");
	return 0;
}

运行结果如下:

./test_stat 
stdin is fifo
stdout is fifo
stderr is fifo

通过以上实验也验证了我们的推测——文件描述符0、1、2是管道或命名管道。

4.4 节主要讨论设置用户ID与用户组ID的问题,书中给出了一些分析,不过我感觉说的不是很清楚,所以给大家分享一篇blog:http://blog.chinaunix.net/uid-18905703-id-3843177.html

关于以上概念,结合我看到的一些资料,给大家做一个简单的总结。关于这几个概念的区分,还是从现象出发,一个程序要执行首先要知道程序的“有效用户ID”、“有效组ID”、“附属组ID”,知道了以上三个“ID”后还不够,我们还要检查当前的用户“ID”是否与以上三个“ID”相匹配,而这个当前的用户“ID”就是“实际用户ID”与“实际组ID”,也就是登录系统时所用到的身份验证信息。一般来说,当执行一个程序文件时,进程的有效用户ID通常就是实际用户ID,有效组ID通常是实际组ID。但也有特殊情况就是书中提到的passwd命令的执行,关于这个命令的详细分析大家可以参考上面那篇blog。

4.5 文件访问权限

这一节主要要给大家分享一下在访问文件过程中所用到的隐藏权限,这里隐藏的权限是指除了访问文件所需要的权限外,还需要的额外的权限,具体内容如下:

  1. 通过文件名打开任意类型的文件时,对文件名(绝对路径)中包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。这里还要强调一下目录的读权限与执行权限的不同意义,读权限允许我们读目录,获得在该目录中所有文件名的列表。当一个目录是我们要访问文件的路径名的一个组成部分时,对该目录的执行权限使我们可通过该目录(也就是搜索该目录,寻找一个特定的文件名)。
  2. 对于一个文件的读权限决定了我们是否能够打开现有文件进行读操作。这与open函数的O_RDONLY和O_RDWR相关。
  3. 对于一个文件的写权限决定了我们是否能够打开现有文件进行写操作。这与open函数的O_WRONLY和O_RDWR相关。
  4. 为了在open函数中对一个文件指定O_TRUNC标志,必须对该文件具有写权限。
  5. 为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。之所以需要执行权限是由于在创建文件前需要查找当前目录中是否已有该文件。
  6. 为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限(删除之前还是要进行查找操作)。对该文件本身则不需要有读、写权限。
  7. 如果用7个exec函数中的任何一个执行某个文件,都必须对该文件具有执行权限。该文件还必须是一个不同文件。

进程每次打开、创建或删除一个文件时,内核就进行文件访问权限测试,这种测试涉及两方面:一方面是文件的所有者,另一方面是执行操作的进程。对于文件的所有者进行管理主要涉及:st_uid(用户ID)、st_gid(用户组ID),对于进程的权限管理需要三个字段:有效用户ID、有限组ID、附属组ID。

有了标识进程与文件权限的信息位,再来就是对这些信息位进行比较,若相符则代表具有权限,若不符则不代表具有相应的权限。以下是内核进行的具体测试过程:

  1. 若进程的有效用户ID是0(超级用户),则允许访问。这给予了超级用户对整个文件系统进行处理的最充分的自由。
  2. 若进程的有效用户ID等于文件的所有者ID(也就是进程拥有此文件),那么如果所有者适当的访问权限位被设置,则允许访问;否则拒绝访问。
  3. 若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果组适当的访问权限位被设置,则允许访问;否则拒绝访问。
  4. 若其他用户适当的访问权限位被设置,则允许访问;否则拒绝访问。

以上4个步骤顺序执行,其中若执行到一个权限规则相匹配则跳出测试,不再进行接下来的测试步骤。

4.6 新文件和目录的所有权

使用open或create创建新文件时,新文件的用户ID设置为进程的有效用户ID。 如果用open或create创建已经存在的文件,则该文件的访问权限位不变。关于组ID可以有以下两个选项:

  1. 新文件的组ID可以是进程的有效组ID。
  2. 新文件的组ID可以是它所在目录的组ID。这种选项的作用是使在某个目录下创建的文件和目录都具有该目录的组ID,于是文件和目录的组所有权从该点向下传递。

对linux操作系统如果设置组ID位被设置则新文件的组ID设置为目录的组ID;否则新文件的组ID设置为进程的有效组ID。

好了,说了这么多有关于权限的问题,让我们回过头来再看看在上一章中留下的问题,当时留下的问题是“先在没有写权限的情况下创建文件,即此时文件仅具有读权限,然后写入数据,根据我的实验若文件不存在则可以成功写入数据。此时文件已经存在,再通过读写权限打开文件则出现无权限错误”。对于后一点错误我们已经可以理解了,确实是没有没有权限,因此后来我们加入了“写权限”,则可以成功写入数据,但对于第一点还不是很明白:“没什么没有写权限却可以写入数据?”。

4.7 access和faccessat函数

先来看看这两个函数的原型:

#include <unistd.h>
extern int access (const char *__name, int __type) __THROW __nonnull ((1));
extern int faccessat (int __fd, const char *__file, int __type, int __flag)
     __THROW __nonnull ((2)) __wur;
这两个函数的功能是按照实际用户ID和实际组ID来测试文件的访问能力,注意此处一定要两个权限都测试成功,才会返回成功,否则则返回失败。再来看看我们能测试哪些权限,在<unistd.h>中有如下定义,上述定义用于函数的type参数。

#define	R_OK	4		/* Test for read permission.  */ //通过注释就可以了解其功能,测试读权限
#define	W_OK	2		/* Test for write permission.  */ //测试写权限
#define	X_OK	1		/* Test for execute permission.  */ //测试执行权限
#define	F_OK	0		/* Test for existence.  */ //测试是否存在

access函数通过其定义可以发现,使用文件名标识被测试的文件。faccessat函数通过fd(指向某个文件夹)与文件名共同确定被测试文件。其中若faccessat函数的__flag参数设定为AT_EACCESS,则访问检查用的是调用进程的有效用户ID和有效组ID(同样按照前面的测试顺序,遇到匹配项就跳出),而不是实际用户ID和实际组ID。

通过书中的示例,对access函数的功能进行简单的测试,源码如下:

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>

int main(int argc,char* argv[])
{
	if( access(argv[1],R_OK)<0 ) perror(argv[0]);
	else printf("read access OK\n");

	if( open(argv[1],O_RDONLY)<0 ) perror(argv[0]);
	else printf("open for reading OK\n");
	return 0;
}

运行结果如下:

./test_access test_access
read access OK
open for reading OK

再来看看/etc/shadow文件的运行情况:

./test_access /etc/shadow
./test_access: Permission denied
./test_access: Permission denied

没有权限运行,所以做一些调整再试试:

su //切换到root用户
chown root test_access //切换用户ID为root,通过这一步操作就可以使用open函数打开文件
chmod u+s test_access //打开用户设置用户ID位,设置用户ID位的作用用一句话概括就是“执行者将具有该程序所有者的权限”,在我们的例子中就是普通用户具有root用户的读
权限
ls -l test_access //此时程序的所有者已经变为root用户,同时设置用户ID位也被设置——“s”。
-rwsrwxr-x 1 root 8712  6月 26 17:36 test_access
exit //返回普通用户
./test_access /etc/shadow
./test_access: Permission denied //本来设置了“s”位,普通用户应该具有与root用户相同的权限,即普通用户同样可以读权限,但因为access函数使用实际用户ID与实际组ID进行
测试,所以出现无权限错误。
open for reading OK //同理由于设置了“s”位,而root用户具有读权限,普通用户具有与root用户相同的权限,则可以通过open函数打开。

(关于ubuntu如何切换到超级用户请见以下blog: http://blog.csdn.net/david_xtd/article/details/7229325)。

 4.8 umask函数

umask函数为进程设置文件模式创建屏蔽字,并返回之前的值(这是少数几个没有出错返回函数中的一个)。函数原型:

#include <sys/stat.h>
extern __mode_t umask (__mode_t __mask) __THROW;
其中,参数cmask是由9个常量中的若干个按位“或”构成的。在文件模式创建屏蔽字中为1的位,在文件mode中的相应位一定被关闭。

通过一个实例感受以下函数的功能,源码如下:

#include <fcntl.h>
#include <unistd.h>

#define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)

int main(int argc,char* argv[])
{
	umask(0);
	if(creat("foo",RWRWRW)<0) perror(argv[0]);
	
	umask(S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
	if(creat("bar",RWRWRW)<0) perror(argv[0]);
	return 0;
}

运行结果如下:

ls -l foo bar
-rw------- 1 andywang andywang 0  6月 26 21:16 bar
-rw-rw-rw- 1 andywang andywang 0  6月 26 21:16 foo

通过实验可以发现,umask(0)没有屏蔽任何字,所以foo文件可以按照我们设定的权限进行创建,但由于在创建bar文件之前通过umask函数屏蔽了S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH,则在创建bar时文件的读写权限仅保留S_IRUSR|S_IWUSR。通过这个实验可以得出结论umask的效果可以一直持续直到进程结束。更改进程的文件模式创建屏蔽字并不影响其父进程(常常是shell)的屏蔽字。但不知道exec函数是否能够延续umask函数的效果,此处留下一个疑问?

4.9 chmod、fchmod、fchmodat函数

先来看看函数原型:

#include <sys/stat.h>
extern int chmod (const char *__file, __mode_t __mode)
     __THROW __nonnull ((1));
extern int fchmod (int __fd, __mode_t __mode) __THROW;
extern int fchmodat (int __fd, const char *__file, __mode_t __mode,
             int __flag)
     __THROW __nonnull ((2)) __wur;

以上三个函数的功能是更改现有文件的访问权限。chmod函数在指定的文件上进行操作,而fchmod函数则对已打开的文件进行操作。fchmodat函数则对已打开的文件进行操作。fchmodat函数与chmod函数在下面两种情况下是相同的:一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。否则,fchmodat计算相对于打开目录(由fd参数指向)的pathname。flag参数可以用于改变fchmodat的行为,当设置了AT_SYMLINK_NOFOLLOW标志时,fchmodat并不会跟随符号链接。

为了改变一个文件的权限位,进程的有效用户ID必须等于文件所有者ID,或者该进程必须具有超级用户权限。

除了前文中提到的9个权限位外还有6个权限位,全部权限如下:

#define	S_ISUID __S_ISUID	/* Set user ID on execution.  */
#define	S_ISGID	__S_ISGID	/* Set group ID on execution.  */
# define S_ISVTX    __S_ISVTX
#define    S_IRUSR    __S_IREAD    /* Read by owner.  */
#define    S_IWUSR    __S_IWRITE    /* Write by owner.  */
#define    S_IXUSR    __S_IEXEC    /* Execute by owner.  */
#define    S_IRWXU    (__S_IREAD|__S_IWRITE|__S_IEXEC)
#define    S_IRGRP    (S_IRUSR >> 3)    /* Read by group.  */
#define    S_IWGRP    (S_IWUSR >> 3)    /* Write by group.  */
#define    S_IXGRP    (S_IXUSR >> 3)    /* Execute by group.  */
/* Read, write, and execute by group.  */
#define    S_IRWXG    (S_IRWXU >> 3)
#define    S_IROTH    (S_IRGRP >> 3)    /* Read by others.  */
#define    S_IWOTH    (S_IWGRP >> 3)    /* Write by others.  */
#define    S_IXOTH    (S_IXGRP >> 3)    /* Execute by others.  */
/* Read, write, and execute by others.  */
#define    S_IRWXO    (S_IRWXG >> 3)


好了,还是结合书中的例子看一下,源码如下:

#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>

int main(int argc,char* argv[])
{
	struct stat statbuf;
	if( stat("foo",&statbuf)<0 ) perror(argv[0]);

	if( chmod("foo", (statbuf.st_mode & ~S_IXGRP) | S_ISGID) <0 ) perror(argv[0]);

	if( chmod("bar", S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH ) <0 ) perror(argv[0]);

	return 0;
}

运行结果如下,还是结合umask函数创建的文件继续进行实验。

ls -l bar foo
-rw------- 1 andywang andywang 0  6月 27 22:31 bar
-rw-r--r-- 1 andywang andywang 0  6月 27 22:31 foo

运行chmod后结果如下:

ls -l bar foo
-rw-r--r-- 1 andywang andywang 0  6月 27 22:32 bar
-rw-rwSrw- 1 andywang andywang 0  6月 27 22:32 foo

先来看看bar文件。比较简单,就是摒弃原来的文件权限,按照新的文件权限进行设置。

再来是foo文件,首先通过stat函数获得已有的文件权限,再来通过操作关闭“组执行”权限,并打开“执行时设置组ID”标志(在ls命令中以“S”体现)。此处要注意的一点是:“S”代表不包括执行位,“s”代表包括执行位。chmod不会影响文件的最后修改时间。

好了,说了这么多让我们来看看文件权限计算到底是怎么进行的,我们已经知道了文件权限标志是如何定义的,让我们来看看具体值:

#define	__S_ISUID	04000	/* Set user ID on execution.  */
#define	__S_ISGID	02000	/* Set group ID on execution.  */
#define	__S_ISVTX	01000	/* Save swapped text after use (sticky).  */
#define	__S_IREAD	0400	/* Read by owner.  */
#define	__S_IWRITE	0200	/* Write by owner.  */
#define	__S_IEXEC	0100	/* Execute by owner.  */

(以上定义位于/usr/include/x86_64-linux-gnu/bits/stat.h中)

好了,文件权限的实际值就是:

#define	S_ISUID __S_ISUID	 //04000,数字以“0”开头,说明是8进制数字
#define	S_ISGID	__S_ISGID //02000
# define S_ISVTX    __S_ISVTX //01000
#define    S_IRUSR    __S_IREAD    //0400
#define    S_IWUSR    __S_IWRITE    //0200
#define    S_IXUSR    __S_IEXEC    //0100
#define    S_IRWXU    (__S_IREAD|__S_IWRITE|__S_IEXEC) // 0700
#define    S_IRGRP    (S_IRUSR >> 3)   //左移三位,变为0040
#define    S_IWGRP    (S_IWUSR >> 3)   //左移三位,变为0020
#define    S_IXGRP    (S_IXUSR >> 3)   //左移三位,变为0010
/* Read, write, and execute by group.  */
#define    S_IRWXG    (S_IRWXU >> 3) //左移三位,变为0070
#define    S_IROTH    (S_IRGRP >> 3)    //在0040的基础上再次左移三位,变为0004
#define    S_IWOTH    (S_IWGRP >> 3)   //0002
#define    S_IXOTH    (S_IXGRP >> 3)    //0001
/* Read, write, and execute by others.  */
#define    S_IRWXO    (S_IRWXG >> 3) //0007

通过上面的计算,可以发现权限位的值与“chmod”命令中所使用的值完全相同,不同权限之间使用“|”运算进行计算,这一点没有什么问题。

再来看看关闭一个标志位所进行的计算,通过上面的例子可以看到,关闭一个权限位所使用的是“先取反,再进行与运算”。还是结合上面的代码来看一看,“S_IXGRP”的值为“0010”编程2进制就是“000001000”(首位的0不参与运算),此时代表仅有“组执行”权限开启,取反后变为“111110111”,仅有“组执行”位关闭,此时进行与运算,则“组执行”位一定会被关闭,而其他权限位则保持不变,如此就实现了关闭“组执行”位的功能。

4.10 粘着位

有关于粘着位的功能请见以下blog:http://os.51cto.com/art/201004/194994.htm

当前,粘着位对于普通文件已经没有作用了,但若目录的粘着位被设置,则可以起到保护其中文件的作用。

4.11 chown、fchown、fchownat、lchown函数

已经看过了更改权限的函数,以上几个函数的主要功能就是改变文件的主人,对应的命令就是chown。与改变权限的那几个函数特点类似,在此就不详细分析了。

4.12 文件长度

stat结构成员st_size表示以字节为单位的文件的长度。此字段只对普通文件、目录文件和符号链接有意义。对于目录,文件长度通常是一个数(如16或512)的整倍数。对于符号链接,文件长度是在文件名中的实际字节数。

同时本小节再次讨论了与文件空洞有关的内容:

在第三章中我们通过实验创建了两个文件,其中一个带有文件空洞,可通过du命令查看文件实际占用的磁盘块个数,命令如下:

du -s file.hole
8	file.hole

由于在我的机器上,POSIXLY_CORRECT没有被设置,所以du命令报告的是512字节块的块数,所以file.hole实际占用的磁盘容量是4096字节,通过wc命令可以查看正常的I/O操作读整个文件长度:

wc -c file.hole
16394 file.hole

该长度与通过ls得到的长度相同。

如果使用实用程序复制这个文件,那么所有这些空洞都会被填满,其中所有实际数据字节皆被填写为0,操作如下:

cat file.hole > file.hole.copy
du file.hole.copy 
20    file.hole.copy //此时有空洞的文件所占用的磁盘块数也是20

好了,说了这么多,通过实验来研究一下,到底st_size的值与那个命令最后得到的值相同,源么如下:

#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>

int main(int argc,char* argv[])
{
	struct stat statbuf;

	if( stat("file.hole",&statbuf)<0 ) perror(argv[0]);
	else printf("%ld\n",statbuf.st_size);

	if( stat("file.nohole",&statbuf)<0 ) perror(argv[0]);
	else printf("%ld\n",statbuf.st_size);

	if( stat("file.hole.copy",&statbuf)<0 ) perror(argv[0]);
	else printf("%ld\n",statbuf.st_size);

	return 0;
}

运行结果如下:

./test_st_size 
16394
16394
16394

与ls命令的结果相同。

4.13 文件截断

将文件截取为一定length可通过以下函数:

#include <unistd.h>
extern int truncate (const char *__file, __off_t __length)
     __THROW __nonnull ((1)) __wur;
extern int ftruncate (int __fd, __off_t __length) __THROW __wur;

以上两个函数定义在unstd.h中,说明上述函数是系统调用,与上面的几个函数具有一定的区别。这里要注意的一点是如果该文件以前的长度大于length,则超过length以外的数据就不再能访问了。如果以前的长度小于length,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0。我认为这个函数的功能与lseek类似,都是直接设置当前文件偏移量。

4.14文件系统

本小节主要介绍unix文件系统(UFS)的概念模型。在学习具体的文件系统之前,应对虚拟文件系统有一定的了解,有关于虚拟文件的介绍请见:

http://blog.csdn.net/u012927281/article/details/51793209

虚拟文件系统与实际文件系统之间的关系请见下图。比较尴尬,只找到一张繁体字的图,下图来自于鸟哥的私房菜。

关于文件系统有以下几点与大家简单分享以下:

  1. 每个i节点中都有一个链接计数,其值是指向该i节点的目录项数。只有当链接计数减至0时,才可删除该文件(也就是可以释放该文件占用的数据块,这里要注意的一点是虚拟文件系统中并没有关于数据块的概念,只有具体的文件系统中才有相关概念)。这也就是为什么“解除对一个文件的链接”操作并不总是意味着“释放该文件占用的磁盘块”的原因。这也是为什么删除一个目录项的函数被称之为unlink而不是delete的原因。在stat结构中,链接计数包含在st_nlink成员中。这种链接为硬链接。
  2. 另外一种链接类型称为符号链接。符号链接文件的实际内容(在数据块中)包含了该符号链接所指向的文件的名字。该i节点中的文件类型是S_IFLINK,于是系统知道这是一个符号链接。
  3. i节点包含了文件有关的所有信息。stat结构中的大多数信息都取自i节点。只有两项重要数据存放在目录项中:文件名和i节点编号。此处的目录项其实也是一种数据块,但它代表的是目录中的一项,因此被称为目录项。目录项中存储的数据是文件名与i节点编号,i节点编号就是文件名所引用的文件的i节点编号,通过这一编号可首先查找到具体的i节点,再通过i节点访问文件中的数据。
  4. 一个文件系统中的i节点不能指向另一个文件系统中的i节点,
  5. 当在不更换文件系统的情况下为一个文件重命名时,该文件的实际内容并未移动,只需构造一个指向现有i节点的新目录项,并删除老的目录项。链接计数并不会发生改变。
  6. 对于任何一个叶子目录(不包含任何其他目录的目录)的链接计数总是2,数值2来自于命令该目录的目录项以及在该目录中的.项。注意,在父目录中的每一个子目录都是该父目录的链接计数增加1。

有关于软链接与硬链接的区别请见:http://www.ibm.com/developerworks/cn/linux/l-cn-hardandsymb-links/

4.15 link、linkat、unlink、unlinkat、remove

创建一个指向现有文件的链接的方法是使用link函数或unlink函数。

extern int link (const char *__from, const char *__to) //若to已经存在则出错。只创建to中的最后一个分量,路径中的其他部分应当已经存在。
     __THROW __nonnull ((1, 2)) __wur;
extern int linkat (int __fromfd, const char *__from, int __tofd,
           const char *__to, int __flags)
     __THROW __nonnull ((2, 4)) __wur;

以上两个函数成功则返回0,若出错则返回-1。由于可能在文件系统中形成循环,因此很多文件系统实现不允许对于目录的硬链接。

为了删除一个现有的目录项,可以调用unlink函数。

extern int unlink (const char *__name) __THROW __nonnull ((1));
extern int unlinkat (int __fd, const char *__name, int __flag)
     __THROW __nonnull ((2));

这两个函数删除目录项,并将由pathname所引用文件的链接计数减1。

若一个文件被某个进程打开,则文件的内容不会被删除,也就是inode节点不会被释放。关闭一个文件时,内核首先检查打开该文件的进程个数;如果这一计数达到0,内核再去检查其链接计数;若计数也是0,那么就删除该文件的内容。

unlink的这种特性经常被程序用来确保即使是在程序崩溃时,它所创建的临时文件也不会被遗留下来。进程用open或create创建一个文件,然后立即调用unlink,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时,该文件的内容才被删除。

如果name是符号链接,那么unlink删除该符号链接,而不是删除由该链接所引用的文件。给出符号链接名的情况下,没有一个函数能删除由该链接所引用的文件。

本节最后一个函数:

#include <stdio.h>
extern int remove (const char *__filename) __THROW;

remove函数的功能是解除对一个文件或目录的链接。对于文件,remove的功能与unlink相同。对于目录,remove的功能与rmdir相同。

4.17 符号链接

有关于符号链接的有关内容,之前已经给大家分享过。相较于符号链接,硬链接具有以下限制:

  1. 硬链接通常要求链接和文件位于同一文件系统中。
  2. 只有超级用户才能创建指向目录的硬链接。

关于能不能以普通用户创建硬链接,可以通过以下实验进行验证,结果如下:

mkdir empty
 ln empty link
ln: "empty": 不允许将硬链接指向目录

再来试试使用超级用户建立指向目录的硬链接,同样不允许,所以此处linux与书中所讲的内容有些许出入。

既然硬链接不行,那就试试软链接:

ln -s empty link

直接就运行通过了,也验证了符号链接的特点之一:可以构建目录的符号链接。

既然可以构建目录的符号链接,那么试试构建指向

4.18节主要介绍创建和读取符号链接的函数,先来看看基本定义。

extern int symlink (const char *__from, const char *__to)
     __THROW __nonnull ((1, 2)) __wur;
extern int symlinkat (const char *__from, int __tofd,
              const char *__to) __THROW __nonnull ((1, 3)) __wur;

以上两个函数用于创建软链接,在创建软链接时并不要求from文件必须存在。因为open函数跟随符号链接,所以需要有一种方法打开该链接本身,并读该链接中的名字。具体函数如下:

extern ssize_t readlink (const char *__restrict __path,
			 char *__restrict __buf, size_t __len)
     __THROW __nonnull ((1, 2)) __wur;
extern ssize_t readlinkat (int __fd, const char *__restrict __path,
               char *__restrict __buf, size_t __len)
     __THROW __nonnull ((2, 3)) __wur;

返回的符号链接的内容不以null字节终止。

4.19与4.20两节主要讨论与文件时间相关的概念,在此先不深入讨论了。

4.21创建与删除目录函数

extern int mkdir (const char *__path, __mode_t __mode)
     __THROW __nonnull ((1));
extern int mkdirat (int __fd, const char *__path, __mode_t __mode)
     __THROW __nonnull ((2));

以上两个函数用于创建目录,其中.和..目录项是自动创建的。目录项通常至少要设置一个执行权限位,以允许访问该目录中的文件名。

用rmdir函数可以删除一个空目录。空目录是只包含.和..这两项的目录。

extern int rmdir (const char *__path) __THROW __nonnull ((1));

4.22 读目录

有关于DIR、struct dirent等结构的详细分析请见:http://www.liweifan.com/2012/05/13/linux-system-function-files-operation/

关于递归读目录程序的相关解析请见:http://blog.csdn.net/angle_birds/article/details/8503039

有些懒了,就不详细分析了。

4.23 chdir、fchdir、getcwd函数

每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。

进程调用chdir和fchdir函数可以更改当前工作目录。

#include <unistd.h>
extern int chdir (const char *__path) __THROW __nonnull ((1)) __wur;
extern int fchdir (int __fd) __THROW __wur;
 因为当前工作目录是进程的一个属性,所以它只影响调用chdir的进程本身,而不影响其他进程。另一方面,由于shell在执行程序时会建立新的进程,因此为了改变shell的当前工作目录可以采取的方法是shell直接调用chdir函数。为此cd命令内建在shell中。

通过以下函数可以获得当前工作目录的完整绝对路径名。

extern char *getcwd (char *__buf, size_t __size) __THROW __wur;

chdir跟随符号链接。


你可能感兴趣的:(APUE读书笔记-第四章 文件和目录)