1. 属性结构体struct stat;
struct stat {
mode_t st_mode; //文件类型与访问权限位
ino_t st_ino; //文件i节点编号
dev_t st_dev; //文件所属的文件系统的设备号(包括主设备、次设备号)
dev_t st_rdev; //文件是块设备、字符设备时的设备号(主设备、次设备)
nlink_t st_nlink; //
uid_t st_uid; //文件所有者的用户Id
gid_t st_gid; //文件所有者的用户组Id
off_t st_size; //文件的大小(字节单位)
time_t st_atime; //文件内容的最新访问时间
time_t st_mtime; //文件内容的最新修改时间
time_t st_ctime; //文件属性的最新修改时间
blksize_t st_blksize;//文件所在文件系统的块大小
blkcnt_t st_blocks; //文件在文件系统中所占的块数
}
2. 文件类型:
1、普通文件
2、目录文件。文件内容为其包括的文件名称与文件i节点编号
3、块特殊文件。文件为块设备
4、字符特殊文件。文件为字符设备
5、FIFO。命名管道,用于进程间通信
6、套接字。用于进程间的网络通信,进程可以是同一台机器上的,也可以是不同机器上的。
struct stat结构体中的st_mode字段,保存有文件的类型,有如下宏可以查看该文件的类型:
S_ISREG(stat.st_mode)。如果返回结果为真,则为普通文件
S_ISDIR(stat.st_mode)。如果返回结果为真,则为目录文件
S_ISCHR(stat.st_mode)。如果返回结果为真,则为字符特殊文件
S_ISBLK(stat.st_mode)。如果返回结果为真,则为块特殊文件
S_ISFIFO(stat.st_mode)。如果返回结果为真,则为命名管道文件
S_ISLNK(stat.st_mode)。如果返回结果为真,则为符合链接文件
S_ISSOCK(stat.st_mode)。如果返回结果为真,则为套接字文件
3. 获取文件属性信息的函数
#include
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstar(int filedes, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
函数返回值:成功返回0,出错返回-1;
pathname为文件路径、fieldes为文件描述符,buf保存文件属性信息
lstat函数,在文件为符号链接文件类型时,只返回该符号链接文件本身对应的文件属性,而不是该符号链接指向的文件的属性。
操作系统中各种文件类型所占比例
一个文件的权限信息如下图所示:
1、第一列的 -rwxr-xr-x:
-:表示该文件是一个普通文件
rwx:文件所有者拥有的权限,此处所有者的操作权限为对读、写、执行
r-x:文件所有组拥有的权限,此处所有组的操作权限为读、执行
r-x:文件其他用户拥有的权限,此处其他用户的操作权限为读、执行
2、第三、四列的 liuxiaowu liuxiaowu:
第三列表示文件所有者,struct stat结构里的st_uid字段
第四列表示文件所有组,struct stat结构里的st_gid字段
3、第五列的 425:
文件的大小,单位为字节
4、第六、七、八列的 Nov 11 23:02
文件内容最后被修改的时间
5、第九列的 index.html:
文件的名称
通过struct stat结构中的st_mode字段,查看文件的权限信息的宏:
S_IRUSR(stat.st_mode),结果为真,表示用户可读
S_IWUSR(stat.st_mode),结果为真,表示用户可写
S_IXUSR(stat.st_mode),结果为真,表示用户可执行
S_IRGRP(stat.st_mode),结果为真,表示组可读
S_IWGRP(stat.st_mode),结果为真,表示组可写
S_IXGRP(stat.st_mode),结果为真,表示组可执行
S_IROTH(stat.st_mode),结果为真,表示其他可读
S_IWOTH(stat.st_mode),结果为真,表示其他可写
S_IXOTH(stat.st_mode),结果为真,表示其他可执行
执行权限的意思:
为了打开/usr/include/stdio.h文件,需要对目录/、/usr、/usr/include具有执行权限,然后还需要对stdio.h文件具有适当的权限,这取决于以何种模式打开它(只读、读写等)
目录的执行权限与读权限的区别:
目录的读权限,是允许读取目录中包含的文件列表信息
目录的执行权限,是指当我们要访问文件的路径名的一个组成部分时,对该目录的执行权限使得我们可以通过该目录
相关的ID
实际用户ID:
创建该进程的用户
实际组ID:
创建该进程的用户所在的用户组
有效用户ID:
进程所属所有者
有效组ID:
进程所属所有组
附加组ID:
进程所属附加组。一个进程可以有一个有效组,但可以有多个附加组。附加组是对有效组的补充,可以扩大进程的操作权限
保存的设置用户ID:
进程在执行exec函数时,会将有效用户ID保存在此
保存的设置组ID:
进程在执行exec函数时,会将有效组ID保存在此
文件的设置用户ID与设置用户组ID
当执行一个程序文件时,进程的有效用户ID通常是实际用户ID,有效组ID通常是实际组ID。但可以在文件的st_mode字段中设置一个特殊标志,其含义是“当执行此文件时,将进程的有效用户ID设置为文件所有者的用户ID”,该特殊标志就是文件的设置用户ID(设置组ID同理)。
有了此标志,则一旦程序文件的所有者为超级用户,而且文件的st_mode字段设置了设置用户ID标志,则进程在执行该程序文件时,进程的有效用户ID就为文件所有者,即超级用户了,这样进程具有超级用户的执行权限。如passwd文件在执行时,允许任何用户修改自己的密码,就是利用的该原理。
进程对文件进行操作时的权限检查:
1、若进程的有效用户ID是0(超级用户),则允许访问。
2、若进程的有效用户ID等于文件的所有者ID,则如果文件所有者有对该文件的适当权限,则允许访问。适当权限是指,若进程为读而打开该文件,则所有者读位应为1;若进程为写而打开该文件,则所有者写位应为1;若进程将执行该文件,则所有者执行位应为1
3、若进程的有效组ID或者附加组ID之一等于文件所有组ID,则如果文件所有组有对该文件的适当权限,则允许访问。
4、若其他用户具有对该文件的适当权限,则允许访问
在执行文件操作时,会按顺序执行以上四步。如果进程的有效用户ID等于文件的所有者ID,则当文件所有者没有对该文件的适当权限,那么是不会继续检查第3步的。依次类推,只会从上至下检查一个步骤。
进程在创建新文件时,文件的所有者默认为进程的有效用户ID。而文件的所有组,在Linux系统下是有如下两种情况:
1.文件所属目录的设置组ID位被设置时,新文件的所有组ID为所属目录的所有组ID
2.否则为进程的有效组ID
当程序文件被执行时,如果文件的设置用户ID(设置组ID同理)位被设置,则执行程序文件的进程的有效用户ID为文件的所有者ID,这时进程的操作权限就变为了文件的所有者ID。此时,如果进程想要判断自己本身的权限是否有对文件的操作权限,则可以用access函数来得到。access函数是使用进程的实际用户ID来判断,是否有对程序文件的操作权限。
#include
int access(const char *name, int mode)
返回值:成功返回0,出错返回-1
mode取值范围,R_OK:可读,W_OK:可写,X_OK:可执行,F_OK:测试文件是否存在
#include
#include
#include
#include
int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (access(argv[i], R_OK) == 0) {
printf("shiji can read\n");//实际用户可读
}
if (open(argv[i], O_RDONLY) > 0) {
printf("youxiao can read\n");//有效用户可读
}
}
}
测试:
1.编译文件:
gcc -std=c99 testAccess.c -o testAccess
2.程序文件的权限信息:
-rwxrwxr-x 1 root liu 7415 11月 26 21:55 testAccess*
3./etc/shadow文件的权限信息:
-rw-r----- 1 root shadow 1220 7月 2 21:59 /etc/shadow
4.测试执行程序文件的进程是否有/etc/shadow的读权限:
$./testAccess /etc/shadow
可以看到,什么也没输出,进程没有相应的操作权限
5.设置程序文件的所有者为root用户以及设置用户ID位:
sudo chown root testAccess
sudo chmod u+s testAccess
6.显示程序文件的权限信息:
-rwsrwxr-x 1 root liu 7415 11月 26 21:55 testAccess*
7.测试执行程序文件的进程是否有/etc/shadow的读权限:
$./testAccess /etc/shadow
youxiao can read
可以看到进程可以以读权限打开文件,这表明进程的实际用户ID没有/etc/shadow文件的操作权限,而进程的有效用户ID有权限,因为此时进程的有效用户ID为程序文件的所有者root
umask值叫做文件模式创建屏蔽值。当创建新文件时,会指定文件的所有组/所有组/其他用户的相应操作权限。新文件最终的操作权限会在指定的权限基础上,减去umask设置值。比如umask为002时,如果指定新文件的权限为 777,则新文件最终的权限为775
#include
int umask(mode_t cmask);
返回值,以前的文件模式创建屏蔽值
#include
int chmod(const char *name, mode_t mode);
int fchmod(int fiedes, mode_t mode);
返回值,成功返回0,出错返回-1
chmod/fchmod函数,用于改变文件的权限位。
只有当进程的有效用户ID等于文件的所有者ID,或者进程具有超级用户权限时,才可以进行修改。
chmod函数只会修改权限信息,不会修改文件内容。这表示其只会更新i节点信息。
mode取值
//所有者读,写,执行,读写执行
S_IRUSR
S_IWUSR
S_IXUSR
S_IRWXU
//所有组读,写,执行,读写执行
S_IRGRP
S_IWGRP
S_IXGRP
S_IRWXG
//其他读,写,执行,读写执行
S_IROTH
S_IWOTH
S_IXOTH
S_IRWXO
//设置用户ID位,设置用户组ID位,设置保存正文(粘住位)
S_ISUID
S_ISGID
S_ISVTX
进程在创建新文件时,如果文件的所有组ID不为进程的有效组ID或附加组ID的某个(父目录设置组ID位时是有可能出现这种情况),则如果新文件设置组ID位,这时会自动将设置组ID位关闭。这就防止了用户创建一个设置组ID文件,而该文件是由非该用户所属组拥有的。防止了用户创建新文件时,设置组ID位,然后以该文件的父目录组的权限执行相应操作。
粘住位的意思是,一旦文件被加载到内存中,则它的信息会一直保存在交换区中,而不会被置换出去。这样下次该文件需要被执行时,可以很快地被加载。
目录的粘住位:
当目录设置了粘住位时,如果用户对目录有写操作权限,则在对该目录下的文件进行删除或更名操作时,必须要满足如下条件之一:
1.用户为超级用户
2.用户为目录所有者
3.用户为文件所有者
如/tmp目录,其权限信息为:
drwxrwxrwt. 9 root root 4096 11月 26 23:13 tmp
可以看到,其所有者为root,所有组为root,其他用户的权限为读写执行(+粘住位)。每个用户都可以在该目录下创建属于自己的文件,但是每个用户只可以对属于自己的文件进行删除或更名操作。
#include
int chown(const char *name, uid_t owner, gid_t gid);
int fchown(int fiedes, uid_t owner, gid_t gid);
int lchown(const char *name, uid_t owner, gid_t gid);
返回值,成功返回0,出错返回-1
如果文件为符号链接,则chown,fchown函数修改的是符号链接指向的文件,而lchown函数修改的是符号链接文件自身。
在函数执行时,如果uid或gid有一个为-1,则所有者或所有组ID不变。
限制规则:
1.只有超级用户进程才可以修改文件的所有者ID
2.在对一个文件调用上述函数执行所有者修改时,必须满足如下两个条件:
1.进程的有效用户ID等于文件的所有者ID
2.owner为-1或为文件的所有者ID时,且进程的有效组ID或附加组ID之一等于gid时,可以设置文件的组ID
当非超级用户修改了文件的所有者ID或所有组ID后,文件的设置用户ID位与设置组ID位都会被清除。这是为了防止修改文件的所有者ID或所有组ID之后,利用文件的设置用户ID位或设置组ID位来执行其他用户的权限。
struct stat结构中的st_size表示文件内容的字节长度,st_blksize表示文件所处的存储系统,最合适的缓冲区块大小,st_blocks表示文件所占512字节的块数量。
空洞:
空洞是指文件中未初始化的数据,比如当直接在文件尾端的后5个字节开始写数据,则在文件尾端后的5个字节,就为空洞,它并不占用实际的存储空间。
但是当把该文件中的数据读取,并写入新的文件时,会将空洞填充为0,这样新文件中,这些原来为空洞的数据就会占用实际的存储空间了。
#include
#include
#include
#include
#include
int main(int argc, char * argv[]) {
int fd;
if ((fd = open("kongdong.txt", O_WRONLY | O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO)) < 0) {
printf("open error: %s\n", strerror(errno));
}
write(fd, "1234", 4);
lseek(fd, 100000, SEEK_END);
write(fd, "5678", 4);
return 0;
}
上述程序执行后,会新建创建文件kongdong.txt,其文件大小为10008字节:
-rwxrwxr-x 1 liuxiaowu liuxiaowu 10008 Dec 2 17:53 kongdong.txt
通过du -s命令,显示该文件所占的块数量为:
8 kongdong.txt
而cat kongdong.txt > kongdong_copy.txt,显示kongdong_copy.txtx文件的大小也为10008字节,但du -s命令,显示该文件所占的块数量为:
-rw-rw-r-- 1 liuxiaowu liuxiaowu 10008 Dec 2 17:54 kongdong_copy.txt
12 kongdong_copy.txt
这说明上述程序生成的文件kongdong.txt是存在空洞的。
#include
#include
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
当length长度比文件原有长度短时,这时会将文件截断,length以后的文件内容将不能再访问。
当length长度比文件原有长度长时,这时会扩展该文件,扩展的文件内容会被读作0,也就是空洞。
上图为文件系统的结构图。一个磁盘分为多个分区,每个分区都是一个文件系统,包括自主块、超级块、柱面组信息。每个柱面包括超级块的副本、i节点与数据块等信息。
i节点在内存中按顺序挨着存放,i节点中包括文件文件类型、文件当前位置、文件大小、文件访问权限、指向文件实际内容所在数据块的指针等信息,struct stat结构中除了i节点编号、文件名称外,其余信息都来自i节点。
数据块是存放文件实际内容的地方,按文件类型不同,又可以细分为目录块与数据块。目录块存放的是目录信息,数据块存放的文件的内容。目录块的格式如图所示,包括i节点编号与文件名称。
每个i节点都有一个引用链接数,表示有多少个目录项指向该i节点。
对于文件来说,只有当其i节点的引用计数为0时,才可以释放该文件所占用的数据空间。这也是为什么删除文件的函数名是unlink,而不是delete
对于目录来说,不包含任何子目录的目录项,其引用链接数至少为2,一个是其目录块本身会有一个目录项指向它自己(称为.),一个是其父目录的目录块会有一个目录项指向其包含的目录项。有包含子目录的目录项,则其引用链接数至少为3,除了上述的两个,还有其子目录的目录块中会有一个指向其父目录的目录项(称为..)。
如上图中的i节点1278,图中显示有两个引用链接数,一个是目录本身(.),一个是其父目录的目录块中的目录项。
由上述内容可知,对于mv命令,其操作过程并没有移动文件内容地址,只是在当前目录中删除了一个目录项,而在另一个目录中增加了一个目录项。
i节点编号是对于某一个文件系统来说的,不同文件系统中i节点编号可能会有重复。
#include
int link(const char *oldpath, const char *newpath);
int unlink(const char *pathname);
#include
int rename(const char *oldpath, const char *newpath);
int remove(const char *pathname);
link函数:
新创建一个目录项newpath,其目录项中的i节点编号与oldpath目录项中的i节点一样。
因i节点编号只在同一个文件系统内有效,link函数不能跨文件系统建立链接。
如果newpath是一个目录,则只有具有超级用户权限时,才可以创建成功。因为对于目录的硬链接,有可能会产生无限循环。
unlink函数
unlink函数,将pathname所在目录项指向i节点的引用链接数减一。
只有当一个文件的引用链接数为0且没有进程打开它时,它的文件空间才能被释放。所以很多临时文件的创建过程如下:先open一个不存在的文件,然后马上unlink它,这时该文件的引用链接数为0,但是该进程还打开它。当进程执行结束时,内核会检查该文件的进程打开数是否为0,如果为0,则继续检查该文件的引用链接数,如果链接数为0,则释放该文件所占用的存储空间。
remove函数
如果pathname是一个文件,则该函数的作用与unlink一样。如果pathname是一个目录,则该函数的作用与rmdir函数一样。
从其头文件stdio.h来看,该函数是ISO C定义的。
rename函数
将oldpath文件更名为newpath。对于文件与目录,作用分别如下:
oldpath与newpath都为文件:
如果newpath已经存在,则会先删除newpath。
删除oldpath目录项,新建newpath目录项。
如果文件为符号链接,则操作的是符号链接本身。
oldpath与newpath都为目录:
如果newpath已存在,则会先删除newpath,这只能是空目录的情况下。
删除oldpath目录项,新建newpath目录项。
newpath目录项的路径,不能是oldpath目录项的子目录。如不能将/usr/foo更名为/usr/foo/foo1,因为这样会删除/usr/foo目录,再新建/usr/foo/foo1目录,这时是不会成功的,因为foo目录已不存在
因为需要删除oldpath目录项,新建newpath目录项,所以需要同时对oldpath的父目录、newpath的父目录有写、执行权限。
而如果newpath已经存在时,进程还需要对newpath有写权限。(还没明白为什么需要有写权限)
硬链接具有下面两个缺点:
1、不能跨文件系统
2、只有超级用户才能创建指向目录的硬链接(因为目录硬链接可能造成无限循环)
基于此,又增加了符号链接。符号链接是一种文件类型,它的内容为指向的实际文件的文件路径。
一般对文件进行操作的函数,都是对符号链接指向的实际文件进行操作,除了特殊的几个函数会对符号链接本身进行操作:
#include
int lstat(const char *restrict path, struct stat *restrict buf);
#include
ssize_t readlink(const char *restrict path, char *restrict buf, size_t bufsize);
符号链接在文件系统中引入循环:
cd APUE/4
mkdir testSymbol
cd testSymbol
mkdir foo
ln -s foo foo/testdir