我们从stat函数开始,逐个说明state结构的每一个成员以了解文件的所有属性。并说明修改这些属性的各个函数(修改所有者、修改权限等)。
stat、fstat和lstat函数
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
一旦给出pathname,stat函数就返回与命名文件有关的信息结构体。fstat函数获取已在描述符fd上打开文件的有关信息。lstat函数类似于stat,但是当命令的文件是一个符号链接时,lstat返回该符号链接的有关信息,而不是由该符号链接引用文件的信息。
statbuf参数时stat结构体指针,它指向一个我们必须提供的结构。这些函数填写由statbuf指向的结构,该结构的实际定义可能有所不同,但基本形式为:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
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
};
该结构体中的每一个成员都是基本系统数据类型。我们将说明此结构体的每个成员以了解文件类型。
使用stat函数最多的可能时ls -l 命令,用其可以获取有关一个文件的所有信息。
文件类型
UNIX系统的大多数文件是普通文件或目录,但也有另外一些文件类型。文件类型包括以下几种:
- 普通文件:这是最常用的文件类型,这种文件包含了某种形式的数据。至于这种数据是文本还是二进制数据对UNIX内核而言并无区别。普通文件内容的解释由处理该文件的应用程序进行。
- 目录文件:这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件,进程必须使用函数才能修改目录。
- 块特殊文件:这种文件类型提供对设备带缓冲的访问,每次访问以固定长度为单位进行。
- FIFO:这种类型文件用于进程间通信,有时也被称为命名管道
- 套接字:这种文件类型用于进程间的网络通信。套接字也可用于一台宿主机上进程之间的非网络通信。
- 符号链接:这种文件类型指向另一个文件。
stat结构体的st_mode成员种,可以使用宏确认文件类型。st_mode是一个16位视图,用于表示文件类型、文件访问权限及特殊权限位。
允许实现将进程间通信IPC对象(消息队列、信号量等)表示为文件。他们的参数并非st_mode,而是指向stat结构的指针。
对每个命令行参数打印文件类型。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
char *filename = argv[1];
struct stat temp;
if(argc < 2) {
perror("ERROR:CMD\r\n");
}
for(int i = 1; i < argc; i++) {
lstat(argv[i], &temp);
printf("file %s :", argv[i]);
if(S_ISREG(temp.st_mode)) {
printf("普通文件\r\n");
} else if(S_ISDIR(temp.st_mode)) {
printf("目录文件\r\n");
} else if(S_ISCHR(temp.st_mode)) {
printf("字符特殊文件\r\n");
} else if(S_ISBLK(temp.st_mode)) {
printf("块文件\r\n");
} else if(S_ISFIFO(temp.st_mode)) {
printf("管道文件\r\n");
} else if(S_ISLNK(temp.st_mode)) {
printf("符号链接\r\n");
} else if(S_ISSOCK(temp.st_mode)) {
printf("套接字\r\n");
} else {
printf("do't know\r\n");
}
}
return 0;
}
这里使用的lstat,而不是stat,因为使用stat将无法检测到文件链接。
其中文件类型,dcb-lsp:分别对应:目录文件、字符文件、块文件、普通文件、字符链接文件、套接字文件、管道文件。
设置用户ID和设置组ID
与一个进程相关的ID有6个或更多。
- 实际用户ID和实际组ID标识我们究竟是谁。这两个字段在登录时取自口令文件种的登陆项。
- 有效用户ID、组ID、附加ID决定了我们的文件访问权限。
- 保存的设置用户ID、组ID在执行一个程序时包含了有效用户ID和有效组ID的副本。
通常有效组ID等于实际用户ID,有效组ID等于实际组ID。
每个文件中的所有者由stat结构体中的st_uid表示,组所有者由结构体st_gid表示。
文件访问权限
每个文件共有9个访问权限位:
我们用u表示用户(所有者)、g表示组、o表示其他。
- 第一个规则:我们用名字打开任何一个文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作路径都应具有执行权限。
注意:对于目录的读权限和执行权限意义是不同的。读权限允许我们读目录,获得在该目录中所有文件名的列表。当一个目录是我们要访问文件的路径名的一个组成部分时,对该目录的执行权限时我们可以通过该目录寻找一个特定的文件名。
当我们不具有执行权限的目录,那么shell绝不会再该目录下找到可执行文件。 - 对于一个文件的读权限决定了我们是否能够打开该文件进行读操作。这与open函数的O_RDONLY和O_RDWR标志相关。
- 对于一个文件的写权限决定了我们能否打开该文件进行操作,与open的O_WRONLY和O_RDWR标志有关。
- 当open函数指定O_TRUNC标志,必须对该文件具有写权限。
- 为了在一个目录中创建新文件,必须对该目录具有写权限和执行权限。
- 为了删除一个文件,对目录必须由写和执行权限,对文件本身没有要求。
进程每次打开、创建或删除一个文件时,内核都会进行文件权限测试,而这种测试可能涉及文件所有者、进程的有效ID以及进程的附加组ID。两个所有者ID时文件的性质,而两个有效ID和附加ID组是进程性质。
内核测试过程:
- 超级用户则允许访问
- 文件的有效用户ID等于实际ID则允许访问。
- 若进程的有效组ID或进程的附加组ID之一等于文件的组ID,若组适当的访问权限制位为设置,则允许访问。
- 若其他用户适当的访问权限位被设置,则允许访问。
新文件和目录的所有权
新文件的用户ID设置为进程的有效用户ID。组ID,POSIX.1允许实现选择以下一个作为新文件的组ID:
- 新文件的组ID是进程的有效组ID
- 新文件的组ID是它所在目录的组ID
access函数
当用open函数打开一个文件时,内核以进程的有效用户ID和有效组ID位基础执行其访问权限测试。但是,有时进程与希望按照实际ID和实际组ID进行访问权限测试。
access函数就是实际用户ID和实际组ID进行访问权限测试的。
#include <unistd.h>
int access(const char *pathname, int mode);
mode为下面表中常量按位或。
测试文件的读写权限。
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
if(argc < 2) {
perror("ERROR: CMD\r\n");
return -1;
}
if(access(argv[1], R_OK) < 0) {
printf("file %s error for read\r\n", argv[1]);
} else {
printf("file %s success for read\r\n", argv[1]);
}
if(access(argv[1], W_OK) < 0) {
printf("file %s error for write\r\n", argv[1]);
} else {
printf("file %s success for write\r\n", argv[1]);
}
return 0;
}
umask函数
umask函数为进程设置文件模式创建屏蔽字,并返回以前的值。
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
可以通过umask控制他们所创建文件的默认权限。
chmod、fchmod函数
chmo命令修改文件的权限
权限分为三种用户:所有者(user)、同组用户(group)、其他用户(other)
权限分为三种:可读(r)、可写(w)、可运行(x)
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
chmod参数pathname即文件名。
fchmod函数设置已在描述符fd上打开文件的权限。
mode:有9项取自9个文件访问权限,以及两个设置ID常量,保存正文常量。
代码实现禁止输入文件的组执行权限:
#include <sys/stat.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
struct stat stat_temp;
if(argc < 2) {
printf("ERROR: CMD\r\n");
return -1;
}
if(stat(argv[1], &stat_temp) < 0) {
printf("ERROR: STAT\r\n");
return -1;
}
if(chmod(argv[1], (stat_temp.st_mode & ~S_IXGRP ) )) {
printf("ERROR: CHMOD\r\n");
return -1;
}
return 0;
}
粘住位
粘住位为S_ISVTX位。理解为程序运行后被粘到交换区了。
如果一个可执行文件这个位被设置了,那么在该程序第一次被执行并结束时,其程序正文部分的一个副本仍被保存在交换区(正文指机器指令部分)。这使得下一次执行该程序时能较快的将其装入内存区中。
其原因是:交换区占用连续磁盘空间,可将它视为连续文件,而且一个程序的正文部分在交换区中也是可以连续的。对于常用的应用程序,我们常常设置它所在的文件的粘住位。
当然交换区可存放的粘住位文件也是有限制的,以免过多占用交换区空间,但无论如何这是一个有用技术。
现在新的Linux系统都有虚拟缓存系统和快速文件系统,所以就不再需要这种技术。
chown、fchown、lchown函数
修改文件的用户ID和组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);
三个函数的区别与stat三个函数的区别类似。
当group、owner参数有任何一个为-1,则对应的ID不变。
只用超级用户才能修改一个文件的所有者,这样做的原因是防止用户改变文件的所有者从而摆脱磁盘空间限制对他们的限制。
文件长度
state结构体成员st_size表示以字节为长度的文件长度。此字段只对普通文件、目录文件和符号链接有意义。
对于普通文件,其文件长度可以是0,在读这种文件时,将得到文件结束指示。
对于目录文件,文件长度通常是一个数的倍数。
对于符号链接,文件长度时文件名中的实际字节数。
LInux提供st_blksize和st_blocks字段即块大小,块数量(所分配的实际512字节块数量)不同的版本块数量单位不一样。
文件空洞
普通文件可以包括空洞,空洞是由所这设置的偏移量超过文件尾端,并写入某些数据后造成的。
对于没有写过的字节位置,read函数读到的字节是0。
使用wc和ls的结果可能不一样,所以说明,文件系统使用了若干块以存放指向实际数据块的各个指针。
文件截短
我们有时候需要在文件末尾端截去一些数据以及缩短文件。将一个文件清空也是一个特例,在文件打开时设置属性为O_TRUNC标志就可以。
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
成功返回0,不成功返回-1
两个函数将都把现有的文件长度截断length字节。如果该文件以前的长度大于length,则超过length以外的数据不再能访问。如果以前的长度小于length,则其效果与系统相关。遵循XSI系统慧增加该文件的长度。若UNIX系统实现扩展了该文件,则在以前文件尾端和新的文件尾端之间的数据将读作为0(在文件中创建一个空洞)。
文件系统
文件系统是文件或数据的存储和管理。对数据实现不同的存储和管理。
目前,正在使用的UNIX文件系统有很多种实现。例如,Solaris支持多种不同类型的磁盘文件系统:传统的基于BSD的UNIX文件系统(UFS),读,写DOS格式化磁盘的文件系统(PCFS),以及读CD的文件系统(HSFS)。UFS是以BSD快速文件系统为基础的。
我们可以把一个磁盘分成一个或多个分区。每个分区可以包含一个文件系统。
分区下是多个柱面组,以及前面的描述信息(自举块、超级块等)。
将柱面组展开:描述信息、i node位图、数据块位图、i node节点(几乎文件所有内容)、数据块
i节点是固定长度的记录项,它包含有关文件的大部分信息:
图中两个目录指向同一个i节点,每个i节点都有一个链接计数,其值是指向该i结点的目录项数。只有当链接计数减小至0时,才可删除文件(需要释放该文件占用的数据块),否则会出现野指针。这就是为什么“解除对一个文件的链接”操作并不意味着“释放该文件占用的磁盘块”的原因。并也是为什么删除一个目录项的函数被称为unlink(断开连接)而不是delete。
在stat结构体中,链接计数包含在st_nlink中,其基本系统类型是nlink_t。这种链接类型为硬链接。并且有最大值LINK_MAX。
i node位图表示这个i node是否使用,块位图也是表示对应的数据块是否使用(0.1表示)位图基本都是用于加速的操作。
i节点包含大多数与文件有关的信息:文件类型、文件访问权限位、文件长度和指向该文件所占用的数据块的指针等。stat结构中的大多数信息都取自i节点。只有两项数据存放在目录项中,文件名和i节点编号;以及包括亚数据、无关数据(隐藏起来的数据)、数据指针(12个指针,每个指针指向一个数据块)。i节点编号的数据类型是ino_t。
如果数据指针12个指针不足以存放所有内容,会选取指针更为一级、二级、三级指针,在32位系统中,每个指针代表4字节,共可以表示256个位,表示256个数据块。
文件名存放在目录文件中,目录中包括多个目录项,每个目录项包括i node节点号、文件名。
每个文件系统各自对他们的i节点进行编号,因此目录项中的i节点编号数指向同一文件系统中的相应i节点,不能使一个目录项指向另一个文件系统的i节点。
当在不更换文件系统情况下为一个文件更名时,该文件的实际内容并未移动,只需构造一个指向现有i节点的新目录项,并解除与旧目录项的链接。
link、ulink、remove、rename函数
link
任何一个文件可以有多个目录项指向其i节点。创建一个指向现有文件的链接的方法是使用link函数。
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
成功返回0,不成功返回-1
此函数创建了一个新项目项,它引用现有的文件oldpath。若newpath已经存在,则返回出错。只创建newpath中的最后一个分量,路径中的其他部分应当已经存在。
创建目录项以及增加链接计数应当是个原子操作。如果实现支持创建指向一个目录的硬链接,但大多数实现要求这两个路径名在同一个文件系统中。大多数实现要求这两个路径在同一个文件系统中。如果实现支持创建指向一个目录的硬链接,那么也仅限于超级用户才可以这样做。 其理由是这样做可能在文件系统中形成循环,大多数处理文件系统的实用程序都不能处理这种。因此很多文件系统不允许对目录进行硬链接。
ulink
unlink删除,为删除一个现有的目录项。
#include <unistd.h>
int unlink(const char *pathname);
成功返回0,不成功返回-1
此函数删除项目将,并将由pathname所引用文件的连接计数减1。如果还有指向该文件的其他链接,则仍可以通过其他链接访问该文件的数据。如果出错,则不对该文件做任何更改。
为了解除对文件的链接,必须对包含该目录项的目录具有写和执行权限。如果对该目录设置了粘住为,则对该目录必须具有写权限,并具备下面三个条件之一:
- 拥有该文件
- 拥有目录
- 具有超级用户特权
只有当链接计数为0时,该文件的内容才可被删除。另外当进程打开了该文件,其内容也不能删除。关闭一个文件时,内核首先检查打开该文件的进程数。如果该数达到0时,然后检查链接数,如果这个数也是0,那么就删除该文件的内容。
如果pathname是符号链接,那么unlink只是删除该符号链接,而无法删除由该链接所引起的文件。没有一个函数能删除由符号链接所引用的文件。
超级用户可以用unlink删除目录,但是删除目录一般使用rmdir。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <unistd.h>
#include <stdio.h>
int main() {
if(open("text.txt", O_RDWR) < 0) {
printf("ERROR OPEN \r\n");
return -1;
}
if(unlink("text.txt") < 0) {
printf("UNLINK ERROR\r\n");
return -1;
}
printf("unlinked\r\n");
sleep(15); // 等待15s
printf("done\r\n");
return 0;
}
硬链接
可以理解为:两个指针指向同一个空间。目录项的同义词。
符号链接
符号链接是指向一个文件的间接指针。它硬链接有所不同,硬链接直接指向文件i节点,引入符号链接的原因是为了避开硬链接的一些限制。
- 硬链接需要要求链接和文件位于同一文件系统中。
- 硬链接不能给目录建立。
- 只有超级用户才能创建指向目录的硬链接。
对符号链接以及它指向何种对象并无任何文件系统限制,任何用户都可创建指向目录的符号链接。符号链接一般用于将一个文件或整个目录机构移植到系统的另一个位置。
当使用以名字引用文件的函数时,应当了解函数是否处理符号链接。也就是该函数是否跟随符号链接到达它所链接的文件。
symlink、readlink函数
symlink函数创建一个符号链接。
#include <unistd.h>
int symlink(const char *target, const char *linkpath);
该函数创建了一个指向target的新目录项linkpath,在创建此符号链接时,并不要求target以及存在,并且target和linkpath并不强制要求在一个文件系统中。
readlink打开符号链接本身。
#include <unistd.h>
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
此函数结合了open、read、close的所有操作,如果此函数执行成功,则它返回读入的buf字节数。在buf的返回字符链接内容不以null字符终止。
文件时间
对每个文件保持有三个时间字段:
mtime是文件内容最后修改的时间,ctime是该文件的i节点最后一次被修改的时间。修改i节点并没有更改文件的实际内容,因为i节点中的所有信息都是与文件的实际内容分开存放的。
修改时间和更改状态时间可被用来归档其内容已经被修改或i节点已经被修改的那些文件。
utime函数
#include <sys/types.h>
#include <utime.h>
int utime(const char *filename, const struct utimbuf *times);
struct utimbuf结构体包括:
struct utimbuf{
time_t actime;
time_t modtime;
}
此结构体中的两个值是日历时间,从1970年1月1日00:00:00以来国际标准时间所经过的秒数。
通过此函数修改文件的数据访问时间和数据修改时间。
mkdir和rmdir函数
mkdir
mkdir用于创建一个空目录。
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
mode为文件访问权限。除了写和读权限,目录通常至少要设置1个执行权限位。允许访问该目录下的文件名。
rmdir
#include <unistd.h>
int rmdir(const char *pathname);
rmdir函数可以删除一个空目录,空目录是只包含.和..这两项的目录;
删除有文件的目录,目录需要用到递归的方法。
如果调用此函数使目录的链接计数成为0,并且没有其他进程打开此目录,则释放由此目录占空的空间:
如果在链接计数到达0时,有一个或几个进程打开此目录,则在此函数返回前删除最后一个链接及目录下的.和..项。另外此目录中不能再创建新文件。但是在最后一个进程关闭它之间并不释放此目录。
读目录
对某个目录具有访问权限的任一用户都可以读该目录,但是,为了防止文件产生混乱,只有内核才能写目录。一个目录的写权限位和执行权限位决定了在该目录中能否创建新文件以及删除文件,他们并不表示能否写目录本事(只有内核可以)。
目录的实际格式依赖于UNIX系统,特别时其文件系统的具体设计和实现。
#include <dirent.h>
DIR *opendir(const char *name);
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
#include <dirent.h>
void rewinddir(DIR *dirp);
#include <dirent.h>
int closedir(DIR *dirp);
#include <dirent.h>
long telldir(DIR *dirp);
#include <dirent.h>
void seekdir(DIR *dirp, long loc);
chdir、fchdir、getcwd函数
每个进程都有一个当前工作目录,此目录是搜索所有相关路径名的起点。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。
int chdir(const char *pathname);
int fchrdir(int fd);
这两个分别使用文件名和文件描述符来指定新的工作目录。
因为内核保持有当前工作目录的信息,所以我们能取其当前值。但是,内核位每个进程只保存指向该目录v节点的指针等目录本身的信息,并不保存该目录的完整路径名。
我们需要一个函数(getpwd),它从当前工作目录(.目录项)开始,使用..找到上一级的目录,然后去其目录项,直到该目录项中的i节点编号与工作目录i节点编号相同,这样就找到了对应的文件名。
char *getcwd(char *buf, size_t size);