目录
前面有介绍过两种标准的I/O处理方式,它们都是针对文件处理的。本章将介绍下文件的一些基本属性,文件类型的判断,以及文件系统的一些基本概念。
9.1 文件属性
9.1.1 stat函数族
Linux系统中stat函数族可以用来获取文件的基本信息,它们的定义如下:
#include<sys/stat.h>
int stat(const char *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 fs, const char * pathname, struct stat *restict buf, int flag);
关于这组stat函数,做几点简单的说明:
- 这组函数都是通过文件名或者打开的文件描述符去获取文件的属性信息,结构存放在stat结构体中;
- Stat结构体的定义如下:
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 */
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 */
};
- lstat函数返回的该符号链接的属性信息,而不是符号链接引用文件的信息,关于符号链接后面会由详细的介绍;
- fstatat函数后面的at 和 openat后面at后缀意义相同,这里不再重复介绍。lag 参数控制着是否跟随一个符号链接,默认情况是跟随,即返回符号链接引用文件的信息;而设置为AT_SYMLINK_NOFOLLOW时返回的是这个符号链接文件的信息。
9.1.2 文件属性信息
stat结构体中有一个成员变量是st_mode,这个变量采用bitmap的形式标识这文件的几种属性信息:文件类型,文件权限位,保存设置的用户ID位以及保存正文位。
Macro | Value | Detail |
S_IFMT | 0170000 | 文件 |
S_IFSOCK | 0140000 | Socket |
S_IFLNK | 0120000 | 普通文件 |
S_IFREG | 0100000 | 目录文件 |
S_IFBLK | 0060000 | 块设备 |
S_IFDIR | 0040000 | 目录文件 |
S_IFCHR | 0020000 | 字符设备 |
S_IFIFO | 0010000 | Fifo文件 |
S_ISUID | 04000 | 设置用户ID位 |
S_ISGID | 02000 | 设置用户组ID位 |
S_ISVTX | 01000 | 保存正文位 |
S_IRUSR(S_IREAD) | 00400 | 用户可读权限 |
S_IWUSR(S_IWRITE) | 00200 | 用户可写权限 |
S_IXUSR(S_IEXEC) | 00100 | 用户可执行权限 |
S_IRGRP | 00040 | 用户组可读权限 |
S_IWGRP | 00020 | 用户组可写权限 |
S_IXGRP | 00010 | 用户组可执行权限 |
S_IROTH | 00004 | 其它用户可读权限 |
S_IWOTH | 00002 | 其它用户可写权限 |
S_IXOTH | 00001 | 其它用户可执行权限 |
表9-1 文件属性信息
下面的几节中我们将逐个介绍着四类属性。
9.1.3 文件类型
表9-1中的前8行是文件类型信息。其中S_IFMT是文件掩码,我们可以通过与这个macro进行“&”操作来判断文件类型,后面的七种类型分别对应的就是之前介绍的七种文件。当然,Glibc中也定义好了七个宏分别来判断是否为对应类型,它们的参数就是stat结构中的st_mode。它们分别是:
Macro | File Type |
S_ISREG() | 普通文件 |
S_ISDIR() | 目录文件 |
S_ISCHR() | 字符特殊文件 |
S_ISBLK() | 块特殊文件 |
S_ISFIFO() | 管道或FIFO |
S_ISLNK() | 符号链接 |
S_ISSOCK() | 套接字[C1] |
这样我们就可以通过下面的形式来判断是否为对应类型:
if(S_ISREG(stat.st_mode))
{
printf(“regular file\n”);
}
9.1.4 文件访问权限
1. 文件访问规则
在介绍文件访问权限前,我们先来说说与进程相关联的三组ID:
- 实际用户ID & 实际用户组ID;
- 有效用户ID & 有效用户组ID & 附属ID;
- 保存的设置用户ID & 保存的设置用户组ID。
实际用户ID和实际用户组ID用来标识我们究竟是谁,就是执行这个程序的用户和组,通常取自登录时口令文件中的登录项。
有效用户ID和有效用户组ID用来执行判断文件的访问权限,通常取自实际用户ID&实际用户组ID是相同的。我们再来看看进程是如何执行文件访问权限的:
图 9-1 进程访问文件权限示意
从上图中我们可以看到进程先去判断有效用户ID与文件用户ID是否相同,如果相同,则此进程对此文件拥有文件ID相应的rwx权限;如果用户ID不匹配,则以相同的原理去判断组ID是否匹配,匹配成功,进程对此文件拥有文件组ID相应的rwx权限;否则进程只能拥有文件的其它用户rwx权限。这里的rwx权限就是表9-1中的最后9项。
其中的用户ID和组ID 在 stat 结构中由st_uid和st_gid标识。此外,文件的stat结构中st_mode中还有两项是设置用户ID位(S_ISUID)和设置用户组ID位(S_ISGID)。如果设置用户ID位为true时,通过exec执行文件时,会将进程的有效用户ID设置为用户ID,即st_uid,此时会将进程的有效用户ID保存在“保存的设置用户ID”,等到执行完此文件后将进程的有效用户ID恢复。对文件的设置用户组ID位也是同样的用法。
至此,我们介绍了与进程相关联的三种ID,并说明了它们的用法。我们来举个例子看一下最后的 设置用户ID位是怎么用的:
Linux系统的passwd相关信息放在/etc/passwd口令文件中,且只有超级用户对这个文件才有写权限。那么我们用户是怎么更改用户passwd的呢?
当用户执行passwd命令时,shell会fork出一个子进程,此时该进程的有效用户ID还是用户ID,然后exec程序执行/usr/bin/passwd这个可执行文件。exec发现/usr/bin/passwd这个可执行文件有SUID为,于是会把进程的有效用户ID设置成可执行文件的用户ID,这里就是root,此时进程就获得了root的权限,也就可以对/etc/passwd改写,从而普通用户可以完成密码的修改。exec进程退出后,会将进程的有效用户ID恢复。
需要注意的是:这里讨论的设置用户ID位通常都只用于exec函数执行该文件。
最后我们来对文件的rwx权限及其相关规则做一个简单总结介绍:
- 当我们用名字打开一个文件时,对该名字中包含的每一个目录,包括隐含的当前工作目录都需要有执行权限。例如,为了打开/mnt/temp文件,我们需要对目录/,/mnt具有执行权限;
- 对一个文件的读写权限决定了我们是否能否打开一个文件进行读写。这与open函数的O_RDONLY|O_WRONLY|O_RDWR有关;
- 为了在open函数中指定O_TRUNC标志,必须对该文件有写权限;
- 为了在一个目录中创建或者删除一个文件,必须对该目录有写权限和执行权限;
- 如果用exec函数族执行某个文件,必须对该文件有执行权限,而且还只能是普通文件。
2. 新文件的所有权
上面介绍了文件几组ID,那新创建的文件用户ID和组ID是什么呢?通常新文件的用户ID就是创建文件进程的有效用户ID,而新文件的组ID可以是以下两种之一:
- 进程的有效组ID;
- 所在目录的组ID。
而在linux中取哪种ID 取决于它所在目录的设置组ID是否被设置,如果被设置的话,新文件的组ID 就被设置为所在目录的组ID,否则就是进程的有效组ID。
3. 权限测试
第1节中有介绍到进程去判断是否有一个文件的rwx权限,是根据有效用户ID或者有效用户组ID去比较的,但如果我们想测试下实际用户ID 是否有读写权限有什么办法呢?Linux中可以用access函数去实现这个功能。
#include<unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
//若执行成功返回0,失败返回-1
其中faccessat的参数使用与fstatat类似,这里也不再过多介绍,其中的mode参数取值如下:
R_OK: 测试读权限;W_OK:测试写权限;X_OK: 测试执行权限。
4. 文件模式屏蔽字
在创建一个文件之前,我们可以指定文件模式的屏蔽字,用来屏蔽那些我们不希望新文件拥有的RWX权限。如果对应的屏蔽模式的bit被设成true,那即使在creat 的时候指定了对应的权限也是无用的。
#include<unistd.h>
mode_t unmask(mode_t omask);
//若执行成功返回原先的文件模式创建屏蔽字,失败返回-1
5. sticky bit
介绍到这里,表9-1中所列的文件属性信息只有S_ISVTX 这一项没有介绍了。保存正文位原先是用于将进程的正文段保存在交互区,等下一次在执行时,能较快的加载入内存。但随着分页技术的普及,这一交互功能已经基本不再使用。新的Linux系统中也拓展了这一bit的使用范围。
如果对一个目录设置了sticky bit,那只有对该目录具有写权限的用户并且必须满足下列的条件之一,才能删除或重命名该目录下的文件:
- 拥有此文件;
- 拥有此目录;
- 是超级用户。
如果没有设置stick bit 会怎么样? 前面其实也有介绍过:‘’为了在一个目录中创建或者删除一个文件,必须对该目录有写权限和执行权限”。
6. 文件长度
如果在creat一个文件时指定了O_TRUNC标志,那么每次打开该文件时,长度都会被截断为0。其实Linux系统中也可以使用更多元的文件截断方式,它们可以根据用户的需求将文件长度截断为固定大小。
#include<unistd.h>
mode_t truncate(char *pathname, off_t length);
mode_t ftruncate(int fd, off_t length);
//若执行成功返回原先的文件模式创建屏蔽字,失败返回-1
本节的最后我们来看看如何更改文件的访问权限,以及如何更改文件的用户ID,组ID。
7. 更改文件访问权限
Linux系统中可以使用下面的这组函数去更改文件的访问权限,但需要注意的是只有超级用户或者进程的有效ID等于文件的所有ID时才可以在此进程中调用这些接口更改文件权限。
#include<sys/stat.h>
chmod(const char *pathname, mode_t mode)
fchmod(int fd, mode_t mode)
fchmodat(int fd, const char *pathname , mode_t mode)
//若执行成功返回原0,失败返回-1
这组函数的mode参数可以使 RWX相关的9组权限,也可以是设置用户ID位,设置组ID为以及保存正文位。
关于这组函数的异同,关于“f”或者‘’at“后缀,其实含义同前面的stat函数族都大同小异,这里不再赘述。
8. 更改文件用户ID
本节的最后我们来介绍一组用来更改文件用户ID和组ID的函数:
#include<unistd.h>
int chown(const char *pathname, uid_t onwer, gid_t group);
int fchown(int fd, uid_t onwer, gid_t group);
fchown at(int fd, const char *pathname , uid_t onwer, gid_t group)
int lchown(const char *pathname, uid_t onwer, gid_t group);
//若执行成功返回原0,失败返回-1
对这组函数简要说明如下:
- 如果owner或者group有一个参数为-1,则表示不去更改对应项;
- 超级用户进程可以更改文件的用户ID和文件组ID;
- 如果 _POSIX_CHOWN_RESTRICTED 常量被指定,虽然不能更改其他用户文件的用户ID但更改你所拥有的文件的组ID,且只能改到你所属的组。这意味着:进程的有效ID必须等于文件ID,参数owner等于-1或者文件用户ID,参数group等于进程的有效组ID或者进程的附属组ID。
9.2 文件链接
Linux中有两种链接类型的文件,我们习惯性的称他们为硬链接和符号连接。
符号连接(symbol link),又称之为软连接,其实它也是一个文件,只是文件的内容是指向另一个文件的链接。
硬链接(hard link),其实它只是一个另一个文件的别名,与指向的文件拥有相同的inode。与软连接相比,硬链接至少存在下面的两点限制:
- 不能跨文件系统,这个很好理解,不同的文件系统,inode可能会不相同;
- 除了超级用户外,不能对目录建立hard link,否则容易陷入死循环。
9.2.1 硬链接的创建与删除
我们可以用下面的这组函数来创建和删除一个指向现有文件的硬链接。
#include<unistd.h>
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int fd, const char *newpath, int flag);
//若执行成功返回0,失败返回-1
说明如下:
- 如果newpath已经存在,则返回出错;而且只创建newpath中最后一个分量,路径中的其它部分应当已经存在。
- 对于linkat函数的at 后缀含义和 flag参数同之前介绍的各函数相同;
- 创建成功后,原来pathname的文件对应的链接计数加1。
#include<unistd.h>
int unlink(const char *pathname);
int linkat(int fd, const char *pathname, int flag);
//若执行成功返回0,失败返回-1
关于删除函数,说明如下:
删除成功后,对原文件的链接计数减1,只有当文件的链接计数为0时才可以删除该文件。另外需要主要的是如果有进程打开了该文件,其内容也不能删除。
为了解除该链接,需要对包含该目录项的所有目录拥有写和执行权限,如果目录被设置了保存正文位,则对该目录必须具有写权限,并且具备下列三种条件之一:
拥有该文件,拥有该目录,具有超级用户权限。
#include<stdio.h>
int remove(const char *pathname);
//若执行成功返回0,失败返回-1
remove函数可以用来解除对一个文件或目录的链接,对于文件remoew 的功能与unlink相同,对于目录,与rmdir相同。
9.2.2 文件或目录重命名
Linux系统中可以使用rename来对一个文件或这目录重命名。
#include<stdio.h>
int rename(const char *oldname, const char *newname);
int renameat(int oldfd, const char *oldname, int newfd, const char *newname);
//若执行成功返回0,失败返回-1
对rename函数,针对oldname是文件,目录或者链接说明如下:
- 如果oldname 是文件,且newname已经存在的情况,那newname不能是一个目录,只能是一个文件名。并且对包含oldname和newname 的各目录项都要具有写权限;
- 如果oldname是目录,且newname已经存在的情况,那newname必须是一个空目录。而且newname不能包含oldname作为新名字的路径前缀,比如/usr/foo不能重命名为/usr/foo/testdir;
- 如果oldname或者newname是符号链接,则处理的是符号链接本身,而不是它引用的文件;
- 不能对.和..重命名;
- 如果oldname和newname引用同一个文件,则函数不做任何更改而直接返回。
9.2.3 符号链接的创建
符号链接的创建可以用下面的这组函数来实现:
#include<unistd.h>
int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
//若执行成功返回0,失败返回-1
这组函数创建了一个指向actualpath的新目录项sympath。不同于创建hard link,这里的actualpath不需要一定存在。
因为open函数是跟随符号链接的,所以linux中也定义了一直函数来打开链接文件本身,并读取该链接中的名字,它们是:
#include<unistd.h>
ssize_t readlink(const char *pathname, char *buf, size_t bufsize);
ssize_t readlinkat(int fd, const char *pathname, char *buf, size_t bufsize);
//若执行成功返回读取的字节数,失败返回-1
这组函数其实是“open,read,close”的原子操作。
9.3 目录
前面的章节中我们介绍过可以用open,fopen去创建一个文件,本节将介绍如何创建一个目录以及如何去读取和更改目录。
9.3.1 创建目录
#include<sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
int mkdir(int fd, const char *pathname, mode_t mode);
//若执行成功返回0,失败返回-1
这两个函数用来创建一个空目录。其中.和..目录项是自动创建的。需要注意的是为了能够访问该目录中的文件名,需要对目录设置执行权限。回忆前面介绍过的目录访问权限,对目录拥有写权限和执行权限的用户可以在该目录中创建文件或者删除文件,但并不表示可以写该目录,实际上,只有内核才能写目录。
我们可以用rmdir来删除一个空目录,但目录真正内核删除的原理同文件一样,需要目录的链接数为0且没有进程打开该目录。
#include<unistd.h>
int rmdir(const char *pathname);
//若执行成功返回0,失败返回-1
9.3.2 读目录
对目录拥有访问权限的用户都可以读该目录。在介绍目录的读取权限前,我们先来看一下目录先关的数据结构DIR和 struct dirent,DIR它类似于文件的FILE结构体。定义如下:
struct __dirstream {
void *__fd;
char *__data;
int __entry_data;
char *__ptr;
int __entry_ptr;
size_t __allocation;
size_t __size;
__libc_lock_define (, __lock)
};
typedef struct __dirstream DIR;
而dirent结构体中不仅包含着目录信息,比较inode,offset等,还包含着目录中拥有的文件信息:
struct dirent
{
long d_ino; /* inode number 索引节点号 */
off_t d_off; /* offset to this dirent 在目录文件中的偏移 */
unsigned short d_reclen; /* length of this d_name 文件名长 */
unsigned char d_type; /* the type of d_name 文件类型 */
char d_name [NAME_MAX+1]; /* file name (null-terminated) 文件名,最长255字符 */
}
同样类似于文件操作,针对目录,Gilbc中也定义了一组函数来打开,读取定位目录等操作。
#include<dirent.h>
DIR *opendir(const char *pathname);
DIR *fopendir(int fd);
//若执行成功返回DIR指针,失败返回NULL
struct dirent *readdir(DIR * dp);
//若执行成功返回strcut dirent指针,若在目录尾或者失败返回NULL
void rewindir(DIR * dp);
int close(DIR * dp);
//若执行成功返回0,失败返回-1
long telldir (DIR * dp );
//返回与dp关联目录的当前位置
Void seekdir(DIR * dp, long loc);
9.3.3 更改当前工作目录
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点。当登录系统时,其当前工作目录取自口令文件(/etc/passwd)中该登录想的第6个字段,用户的其实目录。Linux系统中也定义了一组函数来更改当前的工作目录。
#include<unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
//若执行成功返回0,失败返回-1
此外,linux系统中也提供了一个函数来获取当前工作目录的完整路径。
#include<unistd.h>
char *getcwd(char *buf, size_t size);
//若执行成功返回buf,失败返回NULL
9.4 其它属性
介绍完文件和目录的各种属性,查看9.1.2节中的stat结构体,发现还有一些信息,比如文件长度,文件时间,设备号,本节来查漏补缺,逐一再进行叙述。
9.4.1 文件长度
文件长度在stat结构体中由st_size来表示。此字段只针对普通文件、目录文件以及符号链接才有效。
- 普通文件的长度就是文件中实际内容的长度,从0开始一直到文件结束标志的长度;
- 目录文件的长度通常是一个数(如16或这512)的整数倍;
- 符号链接,文件的偿付是在文件名中的实际字节数。例如usr/lib的长度就是7。
Stat结构体重还是st_blksize,st_blocks这两项,它们分别是对文件I/O较合适的块长度以及所分配的实际512字节块的块数。
9.4.2 文件时间
stat结构体中关于文件时间,也有三个成员来表示,它们分别是:
- st_atime: 文件数据的最后访问时间,可以用”ls –u”命令来查看;
- st_mtime: 文件数据的最后修改时间,可以用”ls ”命令来查看;
- st_ctime: inode状态的最后更改时间,可以用”ls – c” 命令来查看。
当然,我们也可以用下面的这组函数来修改文件的访问以及修改时间。
#include<sys/stat.h>
int futimens(int fd, const stuct timespec times[2]);
int utimensat(int fd, const char *path, const stuct timespec times[2], int fd);
#include<sys/time.h>
utimes(const char *path, const struct timeval times[2]);
//若执行成功返回0,失败返回-1
其中前两个函数使用timespec时间结构体,可以到达ns级的精度,而后一个函数使用timeval时间结构体,可以达到us的精度。
9.4.3 设备特殊文件
最后我们再来介绍下st_dev和st_rdev这两个成员。
- 系统中与每个文件名相关联的st_dev值是文件系统的设备号;
- 只有字符特殊文件和块特殊文件才有st_rdev值,该值包含了实际设备的设备号。
每个文件系统所在的存储设备都由主,次设备号表示。其中主设备号标识设备驱动程序,而次设备号标识特定的子设备。例如,一个磁盘驱动器经常包含若干个文件系统,这些文件系统拥有相同的主设备号,而此设备号则不同。
写在文末:本文作为个人对APUE的学习笔记,章节安排和内容基本参考APUE。文中有疏漏或者错误的地方,还请不吝赐教。