- A+
文件和目录
1. 文件系统
我们可以把一个磁盘分成一个或多个分区,每个分区包含一个文件系统,这个文件系统由很多柱面组成,而柱面中有一个非常重要的概念叫做 i 节点。
i 节点包含了文件的大部分信息,如文件类型,文件访问权限位,文件大小和指向文件数据的指针等,大多数信息都存在st_mode
成员中,有两个重要参数存放在目录中,文件名和 i 节点编号。
2. 文件属性结构体
struct stat { mode_t st_mode; //文件类型和文件权限 uid_t st_uid; //用户ID gid_t st_gid; //组ID nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1 off_t st_size; //文件字节数,即文件大小,链接文件的话是所指路径名的长度 blksize_t st_blksize; //文件系统上进行I/O操作时的最优块大小 blkcnt_t st_blocks; //块数,du命令就是blocks time_t st_atime; //最后一次访问时间,access time_t st_mtime; //最后一次修改时间,modify time_t st_ctime; //最后一次文件属性改变时间,chmod ino_t st_ino; //文件inode结点号 dev_t st_dev; //文件系统的设备号,该文件系统包含了文件名及对应的i节点 dev_t st_rdev; //针对字符设备和块设备,实际的设备号,主/次设备号 };
可以通过以下函数获取文件的信息结构:
#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);
stat
函数可以根据 pathname,获取对应文件的信息结构;fstat
函数,则可以根据文件描述符fd,获取对应文件的信息结构;lstat
函数,针对链接文件,lstat
函数返回的是链接文件本身的信息,而不是链接文件指向的文件的信息;fstatat
函数,可以根据文件描述符 fd 和 pathname 来确定要获取的文件信息,如果获取的是链接文件,flag 参数用来决定获取的是链接文件本身的信息还是链接文件所指向的文件信息。
i-node 表和文件数据的映射关系如下图:
3. 文件信息
st_mode
是 16位的,用二进制表示时,包含三部分信息:
文件类型 设置位 文件权限 **** *** *** *** ***
3.1 文件类型
最常见的文件类型就是普通文件和目录文件了,其次还有一些其他类型的文件:
- 普通文件 (-);
- 目录文件(d);
- 符号链接(l):类似于 windows 中的快捷方式;
- 字符特殊文件(c):这种类型的文件提供了对设备不带缓冲的访问,每次访问长度可变;
- 块特殊文件(b):这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行;
- FIFO(f):有时也叫管道,用于进程间通信;
- 套接字(s):用于进程间的网络通信。
可以通过以下函数来判断文件类型:
#include <sys/stat.h> S_ISREG(); //普通文件,是的话,返回1,参数传入st_mode S_ISDIR(); //目录文件 S_ISLNK(); //符号链接文件 S_ISCHR(); //字符设备文件 S_ISBLK(); //块设备文件 S_ISFIFO(); //FIFO文件 S_ISSOCK(); //套接字
3.2 设置位
设置位分为三种:
设置位 | 功能 |
---|---|
S_ISUID | 执行时会将有效用户ID设置为用户ID |
S_ISGID | 执行时会将有效组ID设置为组ID |
S_ISVTX | 粘着位 |
粘着位只对目录有效,起限制删除的作用,shell
中可以通过chmod +t filename
来设置粘着位。设置了粘着位的目录,只有当操作用户拥有该文件,或拥有该目录,或者是超级用户,且对该目录具有写权限,才能删除或重命名该目录下的文件。
3.3 文件所有权
文件的权限可以分为读,写,执行三种权限,根据不同的所有者可以分为以下三类:
访问权限 | 说明 |
---|---|
S_IRUSR | 用户读 |
S_IWUSR | 用户写 |
S_IXUSR | 用户执行 |
S_IRGRP | 组读 |
S_IWGRP | 组写 |
S_IXGRP | 组执行 |
S_IROTH | 其他读 |
S_IWOTH | 其他写 |
S_IXOTH | 其他执行 |
一般用u
表示用户,用g
表示组,用o
表示其他。
在检查文件权限时,会按照以下顺序执行:
- 对于特权级进程,授予其所有访问权限;
- 若进程 ID 与文件的用户 ID 相同,那么就是文件的属主权限;
- 上述条件不满足的话,会判断进程的组 ID 和文件的组 ID 是否匹配,匹配就赋予文件的属组权限;
- 若以上三点都不满足,内核会根据 other 权限,授予进程相应权限。
对于权限需要注意的点:
- 对于目录来说,目录具备读权限,指的是可以获取该目录下的文件列表名;而目录具备执行权限,指的是能通过目录,所有目录下的文件,举个例子,如果要打开 /usr/include/stdio.h文件,需要对目录 /,/usr,/usr/include 有执行权限。一般目录都需要具备执行权限。
- 如果要在一个目录下创建或删除一个文件,那么该目录需要具备写和执行权限;
- 新文件的用户 ID 设置为进程的有效用户 ID,组 ID 可以使进程的有效组 ID ,也可以是所在目录的组 ID。
3.4 文件权限操作接口
3.4.1 文件权限屏蔽字 umask
在进程创建一个新文件或新目录时,就一定会使用文件模式创建屏蔽字,对应位为 1 的话说明屏蔽对应权限:
屏蔽位 | 含义 |
---|---|
0400 | 用户读 |
0200 | 用户写 |
0100 | 用户执行 |
0040 | 组读 |
0020 | 组写 |
0010 | 组执行 |
0004 | 其他读 |
0002 | 其他写 |
0001 | 其他执行 |
函数如下:
#include <sys/stat.h> mode_t umask(mode_t cmask); //返回之前的文件模式创建屏蔽字
这个参数系统默认就有的,可以通过 shell 命令 umask
查看
umask
3.4.2 文件权限测试 access 和 faccessat
进程打开一个文件时,默认是以进程有效用户 ID 和有效组 ID 来访问的, 但是有时候可能想测试实际用户 ID 和实际组 ID的权限,可以通过这两个函数来实现:
#include <unistd.h> int access(const char *pathname, int mode); int faccessat(int fd, const char *pathname, int mode, int flag); 参数说明: mode: R_OK=测试读权限; W_OK=测试写权限; X_OK=测试执行权限; F_OK=有这个文件吗 返回值: 成功=0; 失败=-1
但因为不是原子的,所以这函数不安全。
3.4.3 文件权限修改函数 chmod,fchmod,fchmodat
#include <sys/stat.h> int chmod(const char *pathname, mode_t mode); int fchmod(int fd, mode_t mode); int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
该函数还可以设置执行时用户 ID 和 组 ID,mode
参数如下:
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. 文件所有者
st_uid
和st_gid
分别指明了文件所属主和所属组。
每个文件都有有一个文件所有者和组所有者,我们把当前的所有者和组所有者分别称为有效用户 ID和有效组 ID,有的情况下可能还有个附属组ID。
一般文件新创建时,其用户 ID 就是创建它的进程的有效用户 ID,文件的组 ID 就是该进程的有效组 ID。
一般情况下,有效用户 ID 就是实际用户 ID,有效组 ID 就是实际组 ID,这两个参数保存在st_uid
和st_gid
中,可以通过函数S_ISUID
和S_ISGID
测试。
与一个进程相关的 ID 梳理如下:
ID 种类 | 说明 |
---|---|
实际用户 ID | 我们实际是谁 |
实际组 ID | |
有效用户 ID | 当前有效的所有者 |
有效组 ID | |
附属组 ID | |
保存的设置用户 ID | 由 exec 函数保存 |
保存的设置组 ID |
文件用户 ID 和组 ID 可以通过以下函数修改,chown
,fchown
,fchownat
和 lchown
:
#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 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); 函数功能: 改变对应文件的用户ID和组ID.
只有特权进程才能使用chown
函数来改变文件的用户 ID,对于非特权进程,如果进程的有效用户 ID 与文件的用户 ID 相匹配,那么可使用chown
函数将文件的组 ID 更换为其从属的任一属组的 ID。
参数owner
或者group
为 -1 时表明不变。
5. 文件链接
结构成员变量st_nlink
代表了文件的硬链接数。
文件链接类型分为硬链接和符号链接,两种区别在于:
- 硬链接通常要求链接和文件在同一文件系统中;
- 只有超级用户才能创建指向目录的硬链接(主要是为了防止在文件系统中引入循环)。
5.1 硬链接
硬链接通常要求链接和文件在同一文件夹,一般也不允许链接到目录,因为链接到目录可能会出现循环的情况,如:
有文件 /temp
在 /temp 中创建一个硬链接文件 link 和普通文件 a,那么遍历文件的时候,可能会出现以下情况:
/temp/a
/temp/link/a
/temp/link/link/a
...
上述情况下,如果是符号链接造成的循环,可以通过unlink
函数解除,因为unlink
不跟随符号链接。
硬链接文件的创建函数如下:
#include <unistd.h> int link(const char *existingpath, const char *newpath); int linkat(int efd, const char *exitingpath, int nfd, const char *newpath, int flag); 参数说明: existingpath: 要创建硬链接的源文件的路径,不应该是符号链接文件的路径; newpath: 创建的硬链接的文件路径,如果该路径已存在,会报错; flag: 通过该标志位控制是否解引用链接文件.
硬链接就是为文件创建一个名字,多个文件名通过相同的 inode 编号,指向同一文件,操作文件时,随便用哪一个都行。
5.2 符号链接
符号链接在使用上的关键就是看函数能不能处理符号链接,一般情况下对符号链接的处理都是处理链接到的文件,而非处理链接文件本身。
5.2.1 符号链接创建函数 symlink,symlinkat
#include <unistd.h> int symlink(const char *pathname, const car *sympath); int symlinkat(const char *pathname, int fd, const char *sympath);
pathname
可以不存在,因为即使存在,也不能保证它不会被删除,就会使该符号链接变为悬空链接,这时,对该符号链接解引用时都会出错。
这里要注意的是symlink
的目标文件的路径不是根据当前路径计算的,而是根据符号链接文件的路径计算的,如:
有目录dir1,dir1中有文件a.txt,现在想在dir1目录下创建a.txt文件的符号链接文件b.txt, 并不是 symlink("./dir1/a.txt", "./dir1/b.txt"); -- 1 而是 symlink("./a.txt", "./dir1/b.txt"); -- 2 1 中写法的话会从 ./dir1/dir1/a.txt 中找 a.txt 文件,这样明显是不正确的 2 中写法的话就是从 ./dir1/a.txt 中找 a.txt 文件
看上去会有点绕,shell 中的ls -s
,命令创建符号链接文件也是这样子的,否则会找不到。
符号链接的长度指的是指向的目标文件的文件名长度。
5.2.2 符号链接读取函数 readlink,readlinkat
常用的open
函数打开链接文件时是跟随符号链接的,所以需要一种方法打开链接文件本身,并读该链接中的名字,就有了以下函数:
#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);
5.2.3 链接文件删除函数unlink()
链接文件的删除必须满足以下两个条件:
- 文件的链接数为 0;
- 当前没有进程打开该文件。
可通过以下函数删除:
#include <unistd.h> int unlink(const char *pathname); int unlinkat(int fd, const char *pathname, int flag);
要注意,unlink
无法删除目录文件,删除目录文件要用rmdir
或remove
函数。
6. 文件大小
stat
结构成员st_size
表示以字节为单位的文件的长度,只对普通文件,目录文件和链接文件有效。
对于普通文件,文件长度可以是 0,在读这类文件时,将得到文件结束(end-of-file)指示;对于目录,文件长度通常是一个数的整数倍,如 16 的整数倍或 512 的整数倍;对于符号链接,文件长度是在文件名中的字节数,对于共享内存对象,该字段表示对象的大小,如:
lib 文件是个链接文件,链接到 usr/lib 那么连接文件 lib 的文件长度为 7,也就是名字长度
st_blksize
是对文件 I/O 较合适的块长度,一般是 4095。
st_blocks
是所分配的实际 512 字节块块数,并不是所有的 OS 都是 512 字节,这个要根据实际情况区分。
对于文件长度,有几种特殊情况:
6.1 文件空洞
文件空洞是文件的偏移量超过文件尾端,并写入了某些数据后造成的。
6.2 文件截断
有时候需要在文件尾端截去一些数据以缩短文件,这种情况下啊就需要用到文件截断,相关函数如下:
#include <unistd.h> int truncate(const char *pathname, off_t length); int ftruncate(int fd, off_t length);
如果文件当前长度大于参数length
,将会截断;如果当前长度小于参数length
,会在文件尾部添加一系列空字节或是一个文件空洞。
du
命令显示的是文件实际所占的块的大小,所以对于有空洞文件,可能会出现st_size
比st_blocks
大的情况,因为st_size
是包含空洞文件的空洞的。
7. 文件的时间
文件的时间可以分为以下三种:
- 文件数据最后访问时间:st_atime;
- 文件数据最后修改时间:st_mtime;
- 文件状态最后改变时间:st_ctime。
需要注意修改时间和文件状态改变的时间的区别,修改指的是文件内容最后一次被修改的时间,状态更改指的是该文件 i 节点最后一次被修改的时间,如文件访问权限,用户ID的更改等。
系统默认按照修改时间排序,也可以通过以下命令修改排序方式:
ls -u //按访问时间排序 ls -c //按状态更改时间排序
文件的访问时间和修改时间是可以通过函数修改的,但是文件状态不行,文件状态发生变化时,会自动更新该字段数据。utime
函数调用成功后会将状态最后改变时间设置为当前时间。
#include <utime.h> int utime(const char *pathname, const struct utimbuf *buf); 参数说明: pathname: 文件所在路径; buf: 要修改的时间值,为NULL的话,设置为当前时间; 返回说明: 成功=0; 失败=-1
utimbuf
结构体定义如下:
struct utimbuf { time_t actime; //访问时间 time_t modtime; //修改时间 };
Linux 还提供了源于 BSD 的utimes
系统调用,其功能类似于utime
:
#include <sys/time.h> int utimes(const char *pathname, const struct timeval tv[2]);
utimes
跟utime
的最大区别在于提供了微秒级别的时间。
还可以使用futimes
函数使用文件描述符来指定文件,对于符号链接文件,可以使用lutimes
函数来对符号链接本身做操作:
#include <sys/time.h> int futimes(int fd, const struct timeval tv[2]); int lutimes(const char *pathname, const struct timeval tv[2]); //不会对符号链接文件解引用
还可以使用纳秒级别的时间修改函数:
#include <sys/stat.h> int futimens(int fd, const struct timespec times[2]); int utimensat(int fd, const char *path, const struct timespec times[2], int flag); 参数说明: times 数组的第一个是访问时间,第二个是修改时间
结构体timespec
定义如下:
struct timespec { time_t tv_sec; //秒 long tv_nsec; //纳秒 };
有以下几种情况:
- times 参数是一个空指针,那么访问时间和修改时间都设置为当前时间;
- times 参数任意一个元素的
tv_nsec
字段是 UTIME_NOW,相应的时间戳设置为当前时间(注意,不是tv_sec
字段); - times 参数任意一个元素的
tv_nsec
字段是 UTIME_OMIT,相应的时间戳保持不变; - 如果既不是 UTIME_NOW,也不是 UTIME_OMIT,相应的时间戳设置为相应的
tv_sec
和tv_nsec
字段的值。
修改的条件是进程对该文件具备写权限,且进程的有效用户 ID 等于 该文件的所有者 ID,或者是超级用户。
对于utimensat
函数,如果fd
指定为AT_FDCWD
,此时对path
参数的解读与utimes
类似。flags
参数一般为 0,但是对于符号链接文件,如果想对链接本身操作,可以将flags
参数设置为AT_SYMLINK_NOFOLLOW
。
8. 设备特殊文件
每个文件系统所在的存储设备都由其主、次设备号表示,可以使用major()
和minor()
分别访问主、次设备号。
系统中与每个文件名关联的st_dev
值是文件系统的设备号,st_ino
字段则该包含文件的 i 节点。利用以上两者,可在所有文件系统中唯一标识某个文件。
只是有字符特殊文件和块特殊文件才有st_rdev
值,此值包含了实际设备的设备号。
9. 通用文件操作函数
9.1 文件重命名 rename/renameat
当不更换文件系统为一个文件重命名时,该文件的实际内容并未移动,只需要构造一个指向现有 i 节点的新目录项,并删除老的目录项。链接计数并不会改变。
#include <stdio.h> int rename(const char *oldname, const char *newname); int renameat(int oldfd, const char *oldname, int newfd, const char *newname);
如果newname
已存在,那么会将其覆盖,如果oldname
和newname
一样,则不发生变化。
以下几点需要注意:
- 如果 oldname 或 newname 指代的是符号链接,那么处理的是符号链接本身,而非链接的文件;
- 如果是对目录文件重命名,要保证
newname
要么不存在,要么存在的目录文件是个空目录,因为rename
是不会移动数据的,且要注意,newname
不能包含oldname
作为路径名前缀; rename
只能用于同一文件系统。
9.2 文件删除 remove,rmdir
rmdir
函数可以删除一个空目录。
#include <unistd.h> int rmdir(const char *pathname);
remove
函数可以用来解除对一个文件或目录的链接,对于文件来说,remove
功能和unlink
一样,对于目录来说,remove
功能和rmdir
一样。
#include <stdio.h> int remove(const char *pathname);
10. 目录文件
10.1 目录的创建和删除
可以使用mkdir
或mkdirat
函数创建目录文件:
#include <sys/stat.h> int mkdir(const char *pathname, mode_t mode); int mkdirat(int fd, const char *pathname, mode_t mode);
目录文件的创建所指定的文件访问权限 mode 由进程的文件模式创建屏蔽字修改(umask)。
常见的错误是指定与文件相同的 mode(只具备读,写权限),但是,对于目录,通常需要执行权限,以允许访问该目录中的文件名。
可以使用rmdir
函数删除目录文件:
#include <unistd.h> int rmdir(const char *pathname);
10.2 读目录
#include <dirent.h> DIR *opendir(const char *pathname);//路径名方式打开一个目录 DIR *fopendir(int fd); //以文件描述符的方式打开一个目录,可以将文件描述符转换为 DIR 结构 struct dirent *readdir(DIR *dp); //读目录,一次返回一条目录 void rewinddir(DIR *dp); //将目录流重新移动到原点 int closedir(DIR *dp); //关闭目录 long telldir(DIR *dp); void seekdir(DIR *dp, long loc);
opendir
函数在执行时会为目录相关联的文件描述符设置close_on_exec
标志,以确保在执行exec
时自动关闭该文件描述符。
dirent
定义如下:
struct dirent { ino_t d_ino; //inode节点号 char d_name[256]; //目录文件名 };
10.3 文件树遍历 nftw()
#define _XOPEN_SOURCE 500 #include <ftw.h> int nftw(const char *dirpath, int (*func)(const char *pathname, const struct stat *statbuf, int typeflag, struct FTW *ftwbuf), int nopenfd, int flags); 参数说明: dirpath: 目录路径; func: 目录树中每个文件要执行的函数; nopenfd: 可使用的文件描述符的最大值,一层一个; flags: 操作标志; 函数功能: 默认以前序遍历方式遍历文件树,并为每个文件调用一个func函数
参数flags
含义如下:
- FTW_CHDIR:在处理目录之前会先调用
chdir
函数进入该目录,主要是配合func
函数使用; - FTW_DEPTH:使用后序遍历;
- FTW_MOUNT:不会越界进入另一个文件系统;
- FTW_PHYS:对符号链接文件不会解引用。
函数func
用的是stat
函数,所以对链接文件,默认是解引用的。
typeflag
参数定义如下:
- FTW_D:目录文件;
- FTW_DNR:是一个不能读取的目录文件;
- FTW_DP:正在对一个目录进行后序遍历,当前项是一个目录,其所包含的文件和子目录已完成处理;
- FTW_F:该文件的类型是除目录和符号链接以外的任何类型;
- FTW_NS:对该文件调用
stat
失败; - FTW_SL:是一个符号链接文件;
- FTW_SLN:是一个悬空的符号链接文件。
参数ftwbuf
是一个FTW
结构型的,定义如下:
struct FTW { int base; //文件名在fpath中的偏移地址 int level; //目录树的层次,也就是深度 };
每次调用func
都需要返回一个整型值,如果返回 0,那么nftw
函数还会继续对树进行遍历,若返回非 0 值,则通知nftw
停止对树的遍历,nftw
函数的返回值与func
的返回值相同。
nftw
内部实现会动态分配内存,所以如果直接通过longjmp
函数跳转出去,至少会引起内存泄露问题。
10.4 工作路径
可以通过chdir
或fchddir
函数来修改当前工作路径:
#include <unistd.h> int chdir(const char *pathname); int fchdir(int fd);
需要注意的是工作路径是进程的一个属性,所以它只影响调用chdir
的进程本身,而不影响其他进程。
可以通过getcwd
函数来获取当前工作路径:
#include <unistd.h> char *getcwd(char *buf, size_t size); 参数说明: buf: 保存返回的当前工作路径; size: 缓冲区大小,超过会报错
如果buf
为 NULL 的话,且size
为 0 的话,系统会按需分配一个缓冲区,并将指向该缓冲区的指针作为函数的返回值。但是此时需要注意用完要释放,否则会造成内存泄漏。
10.5 修改进程的根目录 chroot()
#define _BSD_SOURCE #include <unistd.h> int chroot(const char *pathname);
chroot
函数可以修改当前进程的根目录,主要功能就在于限制当前进程访问其他文件,只能访问当前根目录下的文件。
但是这个机制并不是完全安全的:
对于特权级程序来说,可以使用mknod
函数创建一个内存设备文件,然后可以通过这个内存设备文件访问 RAM 的内容;
对于非特权级程序来说,也要注意以下几种情况:
- 调用
chroot
修改完根目录后,没有立即将当前工作目录更新过来,此时还是可以访问其他文件的; - 如果在调用
chroot
之前有打开一个监禁区外的文件,那么在调用chroot
后可以利用这个文件描述符实现越狱; - 利用域套接字也是可以实现越狱的。
10.6 解析路径名 realpath()
#include <stdlib.h> char *realpath(const char *pathname, char *resolved_path); 参数说明: pathname: 要解析的路径; resolved_path: 保存解析结果; 返回说明: 成功=返回指向结果的指针; 失败=NULL
会对pathname
一 一解析,最后生成绝对路径名。
10.7 解析路径名字符串 dirname()和basename()
#include <libgen.h> char *dirname(char *pathname); //返回一个路径的目录部分 char *basename(char *pathname); //返回一个路径的文件名部分