4.1 引言
本文将描述文件系统的一些特征和文件的性质,从stat函数开始,逐个讲解stat结构的成员以了解文件的属性。
4.2 stat,fstat 和 lstat函数
#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);
返回值:成功返回0;出错返回-1
以上三个函数在 buf 中返回文件的信息结构,fstat获得已在描述符fd上打开文件的有关信息。lstat类似stat,但当命名的文件是一个符号链接时,lstat返回的是符号链接的信息,而不是由该符号链接引用的文件的信息。fstatat为一个相对于当前打开目录(由fd参数指向)的路径名返回文件统计信息。flag控制是否跟随一个符号链接。buf指向一个必须提供的结构:struct stat结构指针,函数会自动填写结构内容。
struct stat{
mode_t st_mode; /* file type & mode (permissions) */
ino_t st_ino; /* i-node 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 byte,for regular files */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t 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*/
};
使用stat函数最多的地方可能就是ls -l命令。
st_mode存储文件类型:
S_ISREG() 普通文件
S_ISDIR() 目录文件
S_ISCHR() 字符特殊文件
S_ISBLK() 块特殊文件
S_ISFIFO() 管道或FIFO
S_ISLNK() 符号链接
S_ISSOCK() 套接字
4.3 文件类型
- 普通文件(regular file) :最常用文件类型,二进制文件也是普通文件。UNIX不区分普通的文本文件和二进制文件。
- 目录文件(directory file):包含其他文件的名字以及这些文件信息的指针。
- 块特殊文件(block special file):这种文件提供对设备带缓冲的访问,每次访问长度固定。
- 字符特殊文件(character special file):这种文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的设备要么是block special file,要么是character special file。
- FIFO:这种文件用于进程间通讯,也称为命名管道(named pipe)。
- 套接字(socket):这种文件用于进程间的网络通讯,也可以用于一台计算机上进程间的非网络通讯。
- 符号链接(symobic link):指向另一个文件。
文件类型的信息包含在stat结构的st_mode
成员中,可以用下列宏进行测试,例如:S_ISREG(buf.st_mode)
。
S_ISREG( ) regular file
S_ISDIR( ) directory file
S_ISBLK( ) block special file
S_ISCHR( ) character special file
S_ISFIFO( ) FIFO
S_ISLINK( ) symobic link
S_ISSOCK( ) socket
另外进程间通信(IPC)对象也可作为文件。用相应的宏进行测试,不过不是用st_mode成员作为参数,而是指向stat的指针。
下面这个程序对每个参数打印文件类型。
#include <stdio.h>
#include <sys/stat.h>
int main(int argc,char* argv[])
{
int i;
struct stat buf;
char *ptr;
for(i = 1;i < argc;i++)
{
if(lstat(argv[i],&buf)< 0)
{
printf("lstat error\n");
continue;
}
printf("%s: ",argv[i]);
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 = "link";
else if(S_ISSOCK(buf.st_mode))
ptr = "socket";
else
ptr = "unknow mode";
printf("%s\n",ptr);
}
return 0;
}
编译运行:
root@debian:/program# ./4-1 /etc/passwd /etc /dev/log /dev/tty /dev/cdrom
/etc/passwd: regular
/etc: directory
/dev/log: socket
/dev/tty: character special
/dev/cdrom: link
root@debian:/program#
4.4 设置用户和组ID
对于一个进程相关的ID由6个或更多
进程相关ID | 含义 |
---|---|
实际用户ID/实际组ID | 我们实际上是谁,这两个字段一般在登录时取自口令文件中的登录项。 |
有效用户ID/有效组ID/附属组ID | 用于文件访问权限检查,决定了我们的文件访问权限。 |
保存的设置用户ID/保存的设置组ID | 由exec函数保存,在执行一个程序是包含了有效用户ID和有效组ID的副本,setuid函数中会用到 |
两个所有者ID是文件的性质,而两个有效ID和附属组ID则是进程的性质。
进程每次打开、创建、删除一个文件时,内核进行文件访问权限测试:
1> 若进程的有效用户ID是0(超级用户)?,是,给充分权限。
2> 若进程的有效用户ID等于文件的所有者ID(进程拥有此文件),那么如果所有者适当的访问权限位被设置,则允许访问;否则拒绝访问。适当访问权限位指的是,进程读打开,则用户读位应为1,写则是用户写位应为1;执行位同。
3> 若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果适当的访问权限被设置,则允许访问。
4> 若其他用户适当的访问权限位被设置,则允许访问,否则拒绝访问。
st_mode值也包含了对文件的访问权限位。每个文件有9个访问权限位:
st_mode值 | 访问权限 |
---|---|
S_IRUSR /S_IWUSR /S_IXUSR | 用户读/用户写/用户执行 |
S_IRGRP /S_IWGRP /S_IXGRP | 组读/组写/组执行 |
S_IROTH /S_IWOTH /SIXIOTH | 他读/他写/他执行 |
文件所有者由stat结构中的st_uid表示,组所有者由st_gid表示。
设置用户ID位和设置组ID位在stat结构的st_mode成员中,可由常量S_ISUID和S_ISGID测试。
4.5 文件访问权限
st_mode值也包含文件访问权限位,前面所提到的文件都有访问权限(access permission)。
每次打开,创建或删除一个文件时,内核都对文件访问权限进行测试,涉及到所有者(st_uid,st_gid)和 进程有效ID(有效用户ID和有效组ID),附加组ID。前两个是文件的性质,后面的是进程的性质,内核进行测试的过程:按顺序测试。
- 若进程的有效ID是0(超级用户),则允许访问。
- 若进程的有效用户ID等于文件的有效用户ID(也就是进程拥有此文件),那么,若所有者适当的访问权限位被设置则允许访问,否则拒绝访问。
- 若进程有效组ID或进程的附加组ID 等于文件的组ID,若适当的访问权限位被设置则允许访问。
- 若其他用户适当的访问权限位被设置,则允许访问。
访问权限使用规则:
1,我们用名字打开任一类型的文件时,对该名字中所包含的每一个目录,包含它可能隐含的当前工作目录都应该有执行权限。这就是为什么对于目录其执行权限位常被搜索位的原因。
2,对于一个文件的读权限决定了我们是否能够打开现有文件进行读操作。与open函数的O_RDONLY和O_RDWR标志有关。
3,对于一个文件的写权限决定了我们是否能够打开现有文件进行写操作。与open函数的O_WRONLY和O_RDWR标志有关。
4,为了在open函数中对一个文件指定O_TRUNC标志,必须对该文件具有写权限。
5,为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。
6,为了删除一个现有文件,必须对包含该目录具有写权限和执行权限。对该文件本身不需要有读写权限。
7,如果用7个exec函数中的任何一个执行某个文件,都必须对该文件具有执行权限。该文件还须是一个普通文件。
4.6 新文件和目录的所有权
当创建新文件或目录时,其文件的用户ID为进程的有效ID,组ID由两种选择:
1, 新文件的组ID可以是进程的有效组ID。
2, 新文件的组ID可以是它所在目录的组ID。
对于Linux 3.2和Solaris 10默认情况下,取决于它所在的目录的设置组ID位是否被设置。如果该目录的这一位已经被设置,则新文件的组ID设置为目录的组ID;否则新文件的组ID设置为进程的有效组ID。
4.7 access 函数
也可以用access函数进行以实际用户或组进行测试(也是四步,有效改为实际)。
<unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
返回值:成功返回0;出错返回-1
其中,如果测试文件已经存在,mode就为F_OK;否则mode按下面所列常量的按位或:
R_OK 测试读权限
W_OK 测试写权限
X_OK 测试执行权限
F_OK 测试文件是否存在
4.8 umusk 函数
umusk为进程设置文件模式创建屏蔽字,并返回以前的值。(此函数没有出错返回)
#include <sys/stat.h>
mode_t umask(mode_t cmask);
返回以前的文件模式创建屏蔽字
参数cmask的值是9个文件访问权限按位或构成的。
任何文件模式创建屏蔽字中为1 的位 ,在文件mode中相应的位都被关闭。
下面的程序创建两个文件,使用不同的文件模式创建屏蔽字。
#include <stdio.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 foo error\n");
umask( S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH );
if(creat("bar",RWRWRW) < 0)
printf("creat bar error\n");
return 0;
}
4.9 chmod、 fchmod 和fchmodat函数
这三个函数可以更改现有文件的访问权限:
#include<sys/stat.h>
int chmod(const char *pathname,mode_t mode);
int fchmod(int filedes,mode_t mode);
int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
成功返回0,出错返回-1。
pathname :指定文件。
filedes:指定文件描述符。
mode:文件访问权限位。
chmod函数在指定的文件上进行操作,而fchmod函数则对已打开的文件进行操作。
mode按下面常量按位或:
S_ISUID 执行时设置用户ID
S_ISGID 执行时设置组ID
S_ISVTX 保存正文(黏着位)
S_IRWXU 用户(所有者)读写和执行
S_IRUSR 用户(所有者)读
S_IWUSR 用户(所有者)写
S_IXUSR 用户(所有者)执行
S_IRWXG 组读、写和执行
S_IRGRP 组读
S_IWGRP 组写
S_IXGRP 组执行
S_IRWXO 他读、写和执行
S_IROTH 他读
S_IWOTH 他写
S_IXOTH 他执行
4.10 粘着位S_ISVTX—(sticky bit)
如果一个可执行程序文件的这一位被设置了,那么该程序第一次被执行,在其终止时,程序正文部分的一个副本仍被保存在交换区(程序的正文部分是机器指令)。
因为系统再次自举前,文件的正文部分总是在交换区中,这正是名字中“粘着”的由来。后来UNIX版本称它为保存正文位(saved-text bit)。现如今较新的UNIX
系统大多数都配置了虚拟存储系统以及快速文件系统,所以不再需要使用这种技术,而扩展了粘着位的使用范围:如果对一个目录设置粘着位,只有对该目录具有
写权限的用户并且满足下列条件之一,才能删除或重命名该目录的文件:
- 拥有此文件
- 拥有此目录
- 是超级用户
4.11 chown fchown fchownat和 lchown函数
这四个函数用于更改文件的用户ID和组ID:
#include<unistd,h>
int chown(const char *pathname,uid_t owner,gid_t group);
int fchown(int filedes,uid_t owner,gid_t group);
int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag);
int lchown(const char *pathname,uid_t owner,gid_t group);
成功返回0,失败返回-1.
lchown 修改的是符号链接本身的所有者。
如果 owner 和group中任意一个为 -1 ,则对应的ID值不变。
如果这些函数由非超级用户调用,则该文件的设置用户ID位和设置组ID位被清除
基于BSD的系统一直规定只有超级用户才能更改一个文件的所有者,这样做的原因是防止用户改变其文件的所有者从而摆脱磁盘空间限额对他们的限制。
System V则允许任一用户更改他们所拥有的文件的所有者。
若_POSIX_CHOWN_RESTRICTED对指定的文件生效,则
- 1,只有超级用户进程能更改该文件的用户ID;
- 2,如果进程拥有此文件(其有效用户ID等于该文件的用户ID),owner等于-1或文件的用户ID,并且group等于进程的有效组ID或进程的附属组ID之一,
那么一个非超级用户进程可以更改该文件的组ID。
这意味着,当_POSIX_CHOWN_RESTRICTED有效时,不能更改其他用户文件的用户ID。
4.12 文件长度
stat 结构成员st_size表示一字节为单位的文件长度。此字段只对普通文件、目录文件和符号链接有意义。
对于符号链接,文件长度就是路劲名长度。
对于普通文件,长度可以为0,而目录,文件长度通常是一个数(16 or 512)的整数倍。
文件中的空洞
空洞是由所设置的偏移量超过了文件尾端,并写入了某些数据后造成
(UNIX中du命令可报告文件所使用的磁盘空间总量,以字节快为统计单位,现在Linux中一般报告的是1024字节块的块数)
4.13 文件截断
有时我们需要在文件尾端截去一些数据以缩短文件。
#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruccate(int fd, off_t length);
return: 0; error:-1.
4.14 文件系统
磁盘:|分区|分区|分区|分区|…
分区->文件系统:|自举块|超级块|柱面组0|柱面组1|…|
柱面组:|超级块副本|配置信息|i节点图|块位图|i节点|数据块|
i节点:|i节点|i节点|i节点|…|
4.15 函数link, linkat, unlink, unlinkat和remove
#include<unistd.h>
int link(const char *existingpath,const char *newpath);
int linkat(int efd, const char *existingpath, const char *newpath, int flag);
返回值:成功,0;出错,-1。
int unlink(const char *pathname);
int unlinkat(int fd, const char *pathname, int flag);
返回值:成功,0;出错,-1。
#include<stdio.h>
int remove(const char *pathname);
返回值:成功,0;出错,-1。
int rename(const char oldname, const char *newname);
int renameat(int oldfd, const char *oldname, int newfd, const char *newname);
返回值:成功,0,出错,-1.
4.17 符号链接
符号链接,是对文件的一个间接指针,硬链接直接指向i节点,使用符号链接是为了避开硬链接的一些限制。
- 硬链接通常要求链接和文件位于同一文件系统中。
- 只有超级用户才能创建指向目录的硬链接(底层文件系统支持下)
4.18 创建和读取符号链接
#inlcude<unistd.h>
int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
返回值:成功,0,出错,-1.
打开链接本身,读链接中的名字。
#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);
返回值:成功,返回读取的字节数;出错,-1.
buf中返回的链接内容不以null字节终止。
4.19 文件的时间
对每个文件维护3个时间字段:
字段 | 说明 | 例子 | ls选项 |
---|---|---|---|
st_atim | 文件数据的最后访问时间 | read | ls -u |
st_mtim | 文件数据的最后修改时间 | write | ls |
st_ctim | i节点状态的最后更改时间 | chmod,chown | ls -c |
一个文件的访问和修改时间可由以下几个函数更改。
#include<sys/stat.h>
int futimens(int fd, const struct timespec time[2]);
int utimensat(int fd, const char *path, const struct timespec time[2], int flag);
int utimes(const char *pathname, const struct timeval times[2]);
返回值:成功,0;出错-1.
struct timeval{
time_t tv_sec; /* seconds */
long tv_usec; /* micreseconds */
};
4.20 函数futimes, utimensat和utimes
一个文件的访问和修改时间可以用下面几个函数更改。可指定到纳秒级精度的时间戳。用到的数据结构同stat函数族相同的timespec结构:
#include <sys/stat.h>
int futimes(int fd, const struct timespec times[2]);
int utimensta(int fd, const char *path, const struct timespec times[2], int flags);
return: 0; error: -1.
时间戳可按下列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字段的值不是前面的标志,tv_nsec和tv_sec发挥作用。
4.21 函数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);
返回值:成功,0;出错,-1。
所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。常见的错误是指定与文件相同的mode(只指定了读写权限),但对于目录通常至少要执行位权限以访问目录总的文件名。
#include<unistd.h>
int rmdir(const char *pathname);
返回值:成功,0;出错,-1.
4.22 读目录
#include<dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
返回值:成功,返回指针;出错,返回NULL
struct dirent *readdir(DIR *dp);
返回值:成功,返回指针;若在目录尾或出错,返回NULL
void rewinddir(DIR *dp);
int closedir(DIR *dp);
返回值:成功,0,出错,-1.
long telldir(DIR *dp);
返回值:与dp关联的目录中的当前位置
void seekdir(DIR *dp,long loc);
DIR是一个内部结构,其作用类似与FILE结构。FILE结构由标准的I/O库维护。
由opendir和dfopendir返回的DIR结构的指针由另外5个函数使用。
struct dirent{
ino_t d_ino; /* i-node number */
char d_name[]; /* null-terminated filename */
}
4.23 函数chdir, fchdir和 getcwd
进程可以调用chdir、fchdir函数更改当前工作目录。
#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
return: 0;error: -1.
获取当前工作目录完整的绝对路径名:
#include <unistd.h>
char *getcwd(char *buf, size_t size);
return: buf; error:NULL.
size为缓冲区的长度(以字节为单位),调用此函数必须保证有足够的长度以容纳绝对路径名再加上一个终止符null字节,否则出错。
4.24 设备特殊文件
st_dev和st_rdev这两个字段经常引起混淆,在编写ttyname函数时会用到这两个字段,有关规则很简单:
1. 每个文件系统所在的存储设备都有其主、次设备号表示。设备号所使用的的数据类型是基本系统数据类型dev_t。主设备号标识设备驱动程序,优势编码为与其通信的外设板;次设备号标识特定的子设备。一个磁盘驱动器经常包含若干个文件系统,在同一磁盘驱动器上的个文件系统通常具有相同的主设备号,但是此设备号却不同。
2. 我们通常可以使用两个宏:major和minor来访问主、次设备号,大多数实现都定义在两个宏中,这就意味着我们无需关系这两个数是如何存放在dev_t对象中的。
3. 系统中与每个文件名关联的st_dev值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的i节点。
4. 只有字符特殊文件和快特殊文件才有st_rdev值。此值包含实际设备的设备号。