本章将讨论 Linux 文件系统的其它特性以及文件相关属性;将从系统调用 stat 开始,可利用其返回一个包含多种文件属性(包括文件时间戳、文件所有权以及文件权限等)的结构体,逐个说明 stat 结构中的每一个成员以了解文件的所有属性,然后介绍用以改变文件属性的各种系统调用;除此之外,还会介绍 Linux 系统中的符号链接以及目录相关的操作。
1 Linux系统中文件类型
Linux 下一切皆文件,文件作为 Linux 系统设计思想的核心理念,在 Linux 系统下显得尤为重要。
在 Windows 系统下,操作系统识别文件类型一般是通过文件名后缀来判断,譬如 C 语言头文件.h、 C语言源文件.c,在 Windows 操作系统下打开文件,首先会识别文件名后缀得到该文件的类型,然后再使用相应的调用相应的程序去打开它。但是在Linux 系统下,并不会通过文件后缀名来识别一个文件的类型,但是文件后缀也要规范、需要根据文件本身的功能属性来添加,譬如 C 源文件就以.c 为后缀、 C 头文件就以.h 为后缀、 shell 脚本文件就以.sh为后缀、这是为了我们自己方便查看、浏览。
Linux 系统下一共分为 7 种文件类型。
1.1 普通文件
普通文件(regular file)在 Linux 系统下是最常见的,譬如文本文件、二进制文件,普通文件中的数据存在系统磁盘中,可以访问文件中的内容,文件中的内容以字节为单位进行存储于访问。
普通文件可以分为两大类:文本文件和二进制文件。
文本文件: 文件中的内容是由文本构成的,所谓文本指的是 ASCII 码字符。文件中的内容其本质上都是数字(因为计算机本身只有0 和 1,存储在磁盘上的文件内容也都是由 0 和 1 所构成),而文本文件中的数字应该被理解为这个数字所对应的 ASCII 字符码;譬如常见的.c、 .h、 .sh、 .txt 等这些都是文本文件,文本文件的好处就是方便人阅读、浏览以及编写。
二进制文件: 二进制文件中存储的本质上也是数字,只不过对于二进制文件来说,这些数字并不是文本字符编码,而是真正的数字。譬如Linux 系统下的可执行文件、 C 代码编译之后得到的.o 文件、.bin 文件等都是二进制文件。
在 Linux 系统下,可以通过 stat 命令或者 ls 命令来查看文件类型,
stat 命令会直观把文件类型显示出来;
对于 ls 命令来说,并没有直观的显示出文件的类型,而是通过符号表示出来,其中第一个字符(’ - ‘)就用于表示文件的类型,减号’ - ‘就表示该文件是一个普通文件;除此之外,来看看其它文件类型使用什么字符表示:
- ‘ - ‘:普通文件
- ‘ d ‘:目录文件
- ‘ c ‘:字符设备文件
- ‘ b ‘:块设备文件
- ‘ l ‘:符号链接文件
- ‘ s ‘:套接字文件
- ‘ p ‘:管道文件
1.2目录文件
目录(directory)就是文件夹,文件夹在 Linux 系统中也是一种文件,是一种特殊文件
1.3 字符设备文件和块设备文件
学过 Linux 驱动编程开发的,对字符设备文件(character)、块设备文件(block)这些文件类型应该并不陌生, Linux 系统下,一切皆文件,也包括各种硬件设备。设备文件(字符设备文件、块设备文件)对应的是硬件设备,在 Linux 系统中,硬件设备会对应到一个设备文件,应用程序通过对设备文件的读写来操控、使用硬件设备,譬如 LCD 显示屏、串口、音频、按键等。
Linux 系统中,可将硬件设备分为字符设备和块设备,所以就有了字符设备文件和块设备文件两种文件类型。虽然有设备文件,但是设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护,当系统关机时,设备文件都会消失;字符设备文件一般存放在 Linux 系统/dev/目录下,所以/dev 也称为虚拟文件系统 devfs。
1.4 符号链接文件
符号链接文件(link)类似于 Windows 系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另一个文件路径,当对符号链接文件进行操作时,系统根据情况会对这个操作转移到它指向的文件上去,而不是对它本身进行操作,譬如,读取一个符号链接文件内容时,实际上读到的是它指向的文件的内容。
1.5 管道文件
管道文件(pipe)主要用于进程间通信
1.6 套接字文件
套接字文件(socket)也是一种进程间通信的方式,与管道文件不同的是,它们可以在不同主机上的进程间通信,实际上就是网络通信。
2 stat函数
Linux 下可以使用 stat 命令查看文件的属性,其实这个命令内部就是通过调用 stat()函数来获取文件属性的, stat 函数是 Linux 中的系统调用,用于获取文件相关的信息,函数原型如下所示(可通过”man 2 stat”命令查看):
1
2
3
4
5
int stat(const char *pathname, struct stat *buf);
pathname: 用于指定一个需要查看属性的文件路径。
buf: struct stat 类型指针,用于指向一个 struct stat 结构体变量。调用 stat 函数的时候需要传入一个 struct stat 变量的指针,获取到的文件属性信息就记录在 struct stat 结构体中,稍后给大家介绍 struct stat结构体中有记录了哪些信息。
返回值: 成功返回0;失败返回-1,并设置 error。
2.1 struct stat结构体
struct stat 是内核定义的一个结构体,在<sys/stat.h>头文件中申明,所以可以在应用层使用,这个结构体中的所有元素加起来构成了文件的属性信息,结构体内容如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 struct stat
{
dev_t st_dev; /* 文件所在设备的 ID */
ino_t st_ino; /* 文件对应 inode 节点编号 */
mode_t st_mode; /* 文件对应的模式 */
nlink_t st_nlink; /* 文件的链接数 */
uid_t st_uid; /* 文件所有者的用户 ID */
gid_t st_gid; /* 文件所有者的组 ID */
dev_t st_rdev; /* 设备号(指针对设备文件) */
off_t st_size; /* 文件大小(以字节为单位) */
blksize_t st_blksize; /* 文件内容存储的块大小 */
blkcnt_t st_blocks; /* 文件内容所占块数 */
struct timespec st_atim; /* 文件最后被访问的时间 */
struct timespec st_mtim; /* 文件内容最后被修改的时间 */
struct timespec st_ctim; /* 文件状态最后被改变的时间 */
};
- st_dev:该字段用于描述此文件所在的设备。不常用,可以不用理会。
- st_ino:文件的 inode 编号。
- st_mode:该字段用于描述文件的模式,譬如文件类型、文件权限都记录在该变量中。
- st_nlink:该字段用于记录文件的硬链接数,也就是为该文件创建了多少个硬链接文件。链接文件可以分为软链接(符号链接)文件和硬链接文件。
- st_uid、 st_gid:此两个字段分别用于描述文件所有者的用户 ID 以及文件所有者的组 ID。
- st_rdev:该字段记录了设备号,设备号只针对于设备文件,包括字符设备文件和块设备文件,不用理会。
- st_size:该字段记录了文件的大小(逻辑大小),以字节为单位。
- st_atim、 st_mtim、 st_ctim:此三个字段分别用于记录文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后被改变的时间,都是 struct timespec 类型变量。
2.2 st_mode变量
st_mode 是 struct stat 结构体中的一个成员变量,是一个 32 位无符号整形数据,该变量记录了文件的类型、文件的权限这些信息,其表示方法如下所示:
重点来看看“文件类型”这 4 个 bit 位,这 4 个 bit 位用于描述该文件的类型,譬如该文件是普通文件、还是链接文件、亦或者是一个目录等,那么就可以通过这 4 个 bit 位数据判断出来,如下所示:
1
2
3
4
5
6
7 S_IFSOCK 0140000 socket(套接字文件)
S_IFLNK 0120000 symbolic link(链接文件)
S_IFREG 0100000 regular file(普通文件)
S_IFBLK 0060000 block device(块设备文件)
S_IFDIR 0040000 directory(目录)
S_IFCHR 0020000 character device(字符设备文件)
S_IFIFO 0010000 FIFO(管道文件)
2.3 struct timespec 结构体
该结构体定义在<time.h>头文件中,是 Linux 系统中时间相关的结构体。应用程序中包含了<time.h>头文件,就可以在应用程序中使用该结构体了,结构体内容如下所示:
1
2
3
4
5 struct timespec
{
time_t tv_sec; /* 秒 */
syscall_slong_t tv_nsec; /* 纳秒 */
};time_t 其实指的就是 long int 类型。
3 fstat和lstat函数
fstat、 lstat 与 stat 的作用一样,但是参数、细节方面有些许不同。
3.1 fstat函数
fstat 与 stat 区别在于, stat 是从文件名出发得到文件属性信息,不需要先打开文件;而 fstat 函数则是从文件描述符出发得到文件属性信息,所以使用 fstat 函数之前需要先打开文件得到文件描述符。、
fstat 函数原型如下(可通过”man 2 fstat”命令查看):
1
2
3
4
5
int fstat(int fd, struct stat *buf);
3.2 lstat函数
lstat()与 stat、 fstat 的区别在于,对于符号链接文件, stat、 fstat 查阅的是符号链接文件所指向的文件对应的文件属性信息,而 lstat查阅的是符号链接文件本身的属性信息。
lstat 函数原型如下所示:
1
2
3
4
5
int lstat(const char *pathname, struct stat *buf);
4 文件属主
Linux 是一个多用户操作系统,系统中一般存在着好几个不同的用户,而 Linux 系统中的每一个文件都有一个与之相关联的用户和用户组,通过这个信息可以判断文件的所有者和所属组。
一般来说文件在创建时,其所有者就是创建该文件的那个用户。 同理,在程序中调用 open 函数创建新文件时也是如此,执行该程序的用户是谁,其文件所有者便是谁 。
文件所属组则表示该文件属于哪一个用户组。 在 Linux 中,系统并不是通过用户名或用户组名来识别不同的用户和用户组,而是通过 ID。 ID 就是一个编号, Linux 系统会为每一个用户或用户组分配一个 ID,将用户名或用户组名与对应的 ID 关联起来,所以系统通过用户 ID(UID)或组 ID(GID)就可以识别出不同的用户和用户组。
文件的用户 ID 和组 ID 分别由 struct stat 结构体中的 st_uid 和 st_gid 所指定。既然 Linux 下的每一个文件都有与之相关联的用户 ID 和组 ID,那么对于一个进程来说亦是如此,与一个进程相关联的 ID 有 5 个或更多,如下表所示:
ID 类型 作用 实际用户 ID 我们实际上是谁 实际组 ID 我们实际上是谁 有效用户 ID 用于文件访问权限检查 有效组 ID 用于文件访问权限检查 附属组 ID 用于文件访问权限检查
实际用户 ID 和实际组 ID 标识我们究竟是谁,也就是执行该进程的用户是谁、以及该用户对应的所属组;实际用户 ID 和实际组 ID 确定了进程所属的用户和组。
进程的有效用户 ID、有效组 ID 以及附属组 ID 用于文件访问权限检查
4.1 有效用户ID和有效组ID
对于有效用户 ID 和有效组 ID 来说,这是进程所持有的概念,对于文件来说,并无此属性!有效用户 ID 和有效组 ID 是站在操作系统的角度,用于给操作系统判断当前执行该进程的用户在当前环境下对某个文件是否拥有相应的权限。
在 Linux 系统中,当进程对文件进行读写操作时,系统首先会判断该进程是否具有对该文件的读写权限,那如何判断呢?自然是通过该文件的权限位来判断, struct stat 结构体中的 st_mode 字段中就记录了该文件的权限位以及文件类型。
当进行权限检查时,并不是通过进程的实际用户和实际组来参与权限检查的,而是通过有效用户和有效组来参与文件权限检查。通常,绝大部分情况下,进程的有效用户等于实际用户(有效用户 ID 等于实际用户 ID),有效组等于实际组(有效组 ID 等于实际组 ID)。
4.2 chown函数
chown 是一个系统调用,该系统调用可用于改变文件的所有者(用户 ID)和所属组(组 ID)。其实在Linux 系统下也有一个 chown 命令,该命令的作用也是用于改变文件的所有者和所属组。
chown 函数原型如下所示(可通过”man 2 chown”命令查看):
1
2
3
int chown(const char *pathname, uid_t owner, gid_t group);
pathname: 用于指定一个需要修改所有者和所属组的文件路径。
owner: 将文件的所有者修改为该参数指定的用户(以用户 ID 的形式描述);
group: 将文件的所属组修改为该参数指定的用户组(以用户组 ID 的形式描述);
返回值: 成功返回 0;失败将返回-1,并且会设置 errno。
虽然该函数用法很简单,但是有以下两个限制条件:
只有超级用户进程能更改文件的用户 ID;
普通用户进程可以将文件的组 ID 修改为其所从属的任意附属组 ID,前提条件是该进程的有效用户ID 等于文件的用户 ID;而超级用户进程可以将文件的组 ID 修改为任意值。
在 Linux 系统下,可以使用 getuid 和 getgid 两个系统调用分别用于获取当前进程的用户 ID 和用户组ID,这里说的进程的用户 ID 和用户组 ID 指的就是进程的实际用户 ID 和实际组 ID。这两个系统调用函数原型如下所示:
1
2
3
4
uid_t getuid(void); gid_t getgid(void);
4.3 fchown和lchown函数
这两个同样也是系统调用,作用与 chown 函数相同,只是参数、细节方面有些许不同。 fchown()、 lchown()这两个函数与 chown()的区别就像是 fstat()、 lstat()与 stat 的区别。
5 文件访问权限
struct stat 结构体中的 st_mode 字段记录了文件的访问权限位。所有文件类型(目录、设备文件)都有访问权限(access permission)。
5.1 普通权限和特殊权限
文件的权限可以分为两个大类,分别是普通权限和特殊权限(也可称为附加权限)。普通权限包括对文件的读、写以及执行,而特殊权限则包括一些对文件的附加权限,譬如Set-User-ID、Set-Group-ID以及Sticky。
普通权限
每个文件都有 9 个普通的访问权限位,可将它们分为 3 类,如下表:
st_mode 权限表示宏 含义 S_IRUSR S_IWUSR
S_IXUSR文件所有者读权限文件所有者写权限文件所有者执行权限 S_IRGRP
S_IWGRP
S_IXGRP同组用户读权限同组用户写权限同组用户执行权限 S_IROTH
S_IWOTH
S_IXOTH其它用户读权限其它用户写权限其它用户执行权限 譬如使用 ls 命令或 stat 命令可以查看到文件的这 9 个访问权限,如下所示:
每一行打印信息中,前面的一串字符串就描述了该文件的 9 个访问权限以及文件类型。
r 表示具有读权限;
w 表示具有写权限;
x 表示具有执行权限;
-表示无此权限。
当进程每次对文件进行读、写、执行等操作时,内核就会对文件进行访问权限检查,以确定该进程对文件是否拥有相应的权限。而文件的权限检查就涉及到了文件的所有者(st_uid)、文件所属组(st_gid)以及其它用户,当然这里指的是从文件的角度来看;而对于进程来说,参与文件权限检查的是进程的有效用户、有效用户组以及进程的附属组用户 。
如何判断权限,首先要搞清楚该进程对于需要进行操作的文件来说是属于哪一类“角色”:
如果进程的有效用户 ID 等于文件所有者 ID(st_uid),意味着该进程以文件所有者的角色存在;
如果进程的有效用户 ID 并不等于文件所有者 ID,意味着该进程并不是文件所有者身份;但是进程的有效用户组ID 或进程的附属组 ID 之一等于文件的组 ID(st_gid),那么意味着该进程以文件所属组成员的角色存在,也就是文件所属组的同组用户成员。
如果进程的有效用户 ID 不等于文件所有者 ID、并且进程的有效用户组 ID 或进程的所有附属组 ID均不等于文件的组 ID(st_gid),那么意味着该进程以其它用户的角色存在。
如果进程的有效用户 ID 等于 0(root 用户),则无需进行权限检查,直接对该文件拥有最高权限。
确定了进程对于文件来说是属于哪一类“角色”之后,相应的权限就直接“对号入座”即可。
特殊权限
st_mode 字段中除了记录文件的 9 个普通权限之外,还记录了文件的 3 个特殊权限,也就是 S 字段权限位, S 字段三个 bit 位中,从高位到低位依次表示文件的 set-user-ID 位权限、 set-groupID 位权限以及 sticky 位权限,如下所示:
特殊权限 含义 S_ISUID set-user-ID 位权限 S_ISGID set-group-ID 位权限 S_ISVTX Sticky 位权限 这三种权限分别使用 S_ISUID、 S_ISGID 和 S_ISVTX 三个宏来表示:
1
2
3 S_ISUID 04000 set-user-ID bit
S_ISGID 02000 set-group-ID bit (see below)
S_ISVTX 01000 sticky bit (see below)这三个权限位具体有什么作用呢?接下里给大家简单地介绍一下:
当进程对文件进行操作的时候、将进行权限检查,如果文件的 set-user-ID 位权限被设置,内核会将进程的有效 ID 设置为该文件的用户 ID(文件所有者 ID),意味着该进程直接获取了文件所有者的权限、以文件所有者的身份操作该文件。
当进程对文件进行操作的时候、将进行权限检查,如果文件的 set-group-ID位权限被设置,内核会将进程的有效用户组ID 设置为该文件的用户组 ID(文件所属组 ID),意味着该进程直接获取了文件所属组成员的权限、以文件所属组成员的身份操作该文件。
Linux 系统下绝大部分的文件都没有设置 set-user-ID 位权限和 set-group-ID 位权限,所以通常情况下,进程的有效用户等于实际用户(有效用户 ID 等于实际用户 ID),有效组等于实际组(有效组 ID 等于实际组 ID)。
5.2 目录权限
删除文件、创建文件这些操作也是需要相应权限的,那这些权限又是从哪里获取的呢?答案就是目录。目录(文件夹)在 Linux 系统下也是一种文件,拥有与普通文件相同的权限方案(S/U/G/O),只是这些权限的含义另有所指。
- 目录的读权限:可列出(譬如:通过 ls 命令)目录之下的内容(即目录下有哪些文件)。
- 目录的写权限:可以在目录下创建文件、删除文件。
- 目录的执行权限:可访问目录下的文件,譬如对目录下的文件进行读、写、执行等操作。
5.3 检查文件权限access
文件的权限检查不单单只讨论文件本身的权限,还需要涉及到文件所在目录的权限,只有同时都满足了,才能通过操作系统的权限检查,进而才可以对文件进行相关操作;所以,程序当中对文件进行相关操作之前,需要先检查执行进程的用户是否对该文件拥有相应的权限。那如何检查呢?可以使用 access 系统调用,函数原型如下:
1
2
3
int access(const char *pathname, int mode);
pathname: 需要进行权限检查的文件路径。
mode: 该参数可以取以下值:
- F_OK:检查文件是否存在
- R_OK:检查是否拥有读权限
- W_OK:检查是否拥有写权限
- X_OK:检查是否拥有执行权限
除了可以单独使用之外,还可以通过按位或运算符” | “组合在一起。
返回值: 检查项通过则返回 0,表示拥有相应的权限并且文件存在;否则返回-1,如果多个检查项组合在一起,只要其中任何一项不通过都会返回-1。
5.4 修改文件权限chmod
在 Linux 系统下,可以使用 chmod 命令修改文件权限,该命令内部实现方法其实是调用了 chmod 函数, chmod 函数是一个系统调用,函数原型如下所示(可通过”man 2 chmod”命令查看):
1
2
3
int chmod(const char *pathname, mode_t mode);
- pathname: 需要进行权限修改的文件路径,若该参数所指为符号链接,实际改变权限的文件是符号链接所指向的文件,而不是符号链接文件本身。
- mode: 该参数用于描述文件权限,与 open 函数的第三个参数一样,这里不再重述,可以直接使用八进制数据来描述,也可以使用相应的权限宏(单个或通过位或运算符” | “组合)。
- 返回值: 成功返回 0;失败返回-1,并设置 errno。
文件权限对于文件来说是非常重要的属性,是不能随随便便被任何用户所修改的,要想更改文件权限,要么是超级用户(root)进程、要么进程有效用户 ID 与文件的用户 ID(文件所有者)相匹配。
fchmod函数
该函数功能与chmod 一样,参数略有不同。 fchmod()与 chmod()的区别在于使用了文件描述符来代替文件路径,就像是 fstat 与 stat的区别。函数原型如下所示:
1
2
3
int fchmod(int fd, mode_t mode);使用了文件描述符 fd 代替了文件路径 pathname,其它功能都是一样的。
5.5 umask函数
umask命令用于查看/设置权限掩码,权限掩码主要用于对新建文件的权限进行屏蔽。权限掩码的表示方式与文件权限的表示方式相同,但是需要去除特殊权限位, umask 不能对特殊权限位进行屏蔽。
当新建文件时,文件实际的权限并不等于我们所设置的权限,譬如:调用 open 函数新建文件时,文件实际的权限并不等于 mode 参数所描述的权限,而是通过如下关系得到实际权限:
1 mode & ~umask譬如调用 open 函数新建文件时, mode 参数指定为 0777,假设 umask 为 0002,那么实际权限为:
1 0777 & (~0002) = 0775
umask 权限掩码是进程的一种属性,用于指明该进程新建文件或目录时,应屏蔽哪些权限位。进程的umask 通常继承至其父进程(关于父、子进程相关的内容将会在后面章节给大家介绍),譬如在 Ubuntu shell终端下执行的应用程序,它的 umask 继承至该 shell 进程。
当然, Linux 系统提供了 umask 函数用于设置进程的权限掩码,该函数是一个系统调用,函数原型如下所示(可通过”man 2 umask”命令查看):
1
2
3
mode_t umask(mode_t mask);
- mask: 需要设置的权限掩码值,可以发现 make 参数的类型与 open 函数、 chmod 函数中的 mode 参数对应的类型一样,所以其表示方式也是一样的,前面也给大家介绍了,既可以使用数字表示(譬如八进制数)也可以直接使用宏(S_IRUSR、 S_IWUSR等)。
- 返回值: 返回设置之前的
- umask 值,也就是旧的 umask。
再次强调, umask 是进程自身的一种属性、 A 进程的 umask 与 B 进程的 umask 无关(父子进程关系除外)。在 shell 终端下可以使用 umask 命令设置 shell 终端的 umask 值,但是该 shell 终端关闭之后、再次打开一个终端,新打开的终端将与之前关闭的终端并无任何瓜葛!
6 文件的时间属性
文件的时间属性:文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后被改变的时间,分别记录在 struct stat 结构体的 st_atim、 st_mtim 以及 st_ctim 变量中,如下所示:
字段 说明 st_atim 文件最后被访问的时间 st_mtim 文件内容最后被修改的时间 st_ctim 文件状态最后被改变的时间
- 文件最后被访问的时间:访问指的是读取文件内容,文件内容最后一次被读取的时间,譬如使用read()函数读取文件内容便会改变该时间属性;
- 文件内容最后被修改的时间:文件内容发生改变,譬如使用 write()函数写入数据到文件中便会改变该时间属性;
- 文件状态最后被改变的时间:状态更改指的是该文件的 inode 节点最后一次被修改的时间,譬如更改文件的访问权限、更改文件的用户 ID、用户组 ID、更改链接数等,但它们并没有更改文件的实际内容,也没有访问(读取)文件内容。为什么文件状态的更改指的是 inode 节点的更改呢? 3.1 小节给大家介绍 inode 节点的时候给大家介绍过, inode 中包含了很多文件信息,譬如:文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(时间属性)、文件数据存储的block(块)等,所以由此可知,状态的更改指的就是 inode 节点内容的更改。譬如 chmod()、 chown()等这些函数都能改变该时间属性。
一些系统调用或 C 库函数对文件时间属性的影响,有些操作并不仅仅只会影响文件本身的时间属性,还会影响到其父目录的相关时间属性 。
6.1 utime(),utimes()修改时间属性
文件的时间属性虽然会在我们对文件进行相关操作(譬如:读、写)的时候发生改变,但这些改变都是隐式、被动的发生改变,除此之外,还可以使用 Linux 系统提供的系统调用显式的修改文件的时间属性。
utime()函数原型如下所示:
1
2
3
int utime(const char *filename, const struct utimbuf *times);
filename: 需要修改时间属性的文件路径。
times: 将时间属性修改为该参数所指定的时间值, times 是一个 struct utimbuf 结构体类型的指针,稍后给大家介绍,如果将 times 参数设置为 NULL,则会将文件的访问时间和修改时间设置为系统当前时间。
返回值: 成功返回值0;失败将返回-1,并会设置 errno。
struct utimbuf 结构体:
1
2
3
4 struct utimbuf {
time_t actime; /* 访问时间 */
time_t modtime; /* 内容修改时间 */
}该结构体中包含了两个 time_t 类型的成员,分别用于表示访问时间和内容修改时间, time_t 类型其实就是 long int 类型,所以这两个时间是以秒为单位的,所以由此可知, utime()函数设置文件的时间属性精度只能到秒。
utimes()也是系统调用,功能与 utime()函数一致,只是参数、细节上有些许不同,utimes()与 utime()最大的区别在于前者可以以微秒级精度来指定时间值,其函数原型如下所示:
1
2
3
int utimes(const char *filename, const struct timeval times[2]);
filename: 需要修改时间属性的文件路径。
times: 将时间属性修改为该参数所指定的时间值, times 是一个 struct timeval 结构体类型的数组,数组共有两个元素,第一个元素用于指定访问时间,第二个元素用于指定内容修改时间,稍后给大家介绍,如果times 参数为 NULL,则会将文件的访问时间和修改时间设置为当前时间。
返回值: 成功返回0;失败返回-1,并且会设置 errno。
1
2
3
4
5 struct timeval
{
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 */
};
6.2 futimens()、 utimensat()修改时间属性
这两个系统调用相对于 utime 和 utimes 函数有以下三个优点:
可按纳秒级精度设置时间戳。相对于提供微秒级精度的 utimes(),这是重大改进!
可单独设置某一时间戳。譬如,只设置访问时间、而修改时间保持不变,如果要使用 utime()或utimes()来实现此功能,则需要首先使用 stat()获取另一个时间戳的值,然后再将获取值与打算变更的时间戳一同指定。
可独立将任一时间戳设置为当前时间。使用 utime()或 utimes()函数虽然也可以通过将 times 参数设置为 NULL 来达到将时间戳设置为当前时间的效果,但是不能单独指定某一个时间戳, 必须全部设置为当前时间(不考虑使用额外函数获取当前时间的方式,譬如 time())。
futimens 函数原型如下所示(可通过”man 2 utimensat”命令查看):
1
2
3
4
int futimens(int fd, const struct timespec times[2]);
fd: 文件描述符。
times 将时间属性修改为该参数所指定的时间值, times 指向拥有 2 个 struct timespec 结构体类型变量的数组。
返回值: 成功返回 0;失败将返回-1,并设置 errno。
utimensat()函数
utimensat()与 futimens()函数在功能上是一样的,同样可以实现纳秒级精度设置时间戳、单独设置某一时间戳、独立将任一时间戳设置为当前时间,与 futimens()在参数以及细节上存在一些差异,使用 futimens()函数,需要先将文件打开,通过文件描述符进行操作, utimensat()可以直接使用文件路径方式进行操作。utimensat 函数原型如下所示:
1
2
3
4
int utimensat(int dirfd, const char *pathname, const struct timespec times[2], int flags);
dirfd: 该参数可以是一个目录的文件描述符,也可以是特殊值 AT_FDCWD;如果 pathname 参数指定的是文件的绝对路径,则此参数会被忽略。
pathname: 指定文件路径。如果 pathname 参数指定的是一个相对路径、并且 dirfd 参数不等于特殊值AT_FDCWD,则实际操作的文件路径是相对于文件描述符 dirfd 指向的目录进行解析。如果 pathname 参数指定的是一个相对路径、并且 dirfd 参数等于特殊值 AT_FDCWD,则实际操作的文件路径是相对于调用进程的当前工作目录进行解析
times: 与 futimens()的 times参数含义相同。
flags : 此 参 数 可 以 为 0 , 也 可 以 设 置 为 AT_SYMLINK_NOFOLLOW , 如 果 设 置 为AT_SYMLINK_NOFOLLOW,当 pathname参数指定的文件是符号链接,则修改的是该符号链接的时间戳,而不是它所指向的文件。
返回值: 成功返回0;失败返回-1、并会设置时间戳
7 符号链接(软链接)
在 Linux 系统中有两种链接文件,分为软链接(也叫符号链接)文件和硬链接文件。
使用 ln 命令创建的两个硬链接文件与源文件 test_file 都拥有相同的 inode 号,既然inode 相同,也就意味着它们指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已,创建出来的硬链接文件与源文件对文件系统来说是完全平等的关系。
软链接文件与源文件有着不同的 inode 号,所以也就是意味着它们之间有着不同的数据块,但是软链接文件的数据块中存储的是源文件的路径名,链接文件可以通过这个路径找到被链接的源文件,它们之间类似于一种“主从”关系,当源文件被删除之后,软链接文件依然存在,但此时它指向的是一个无效的文件路径,这种链接文件被称为悬空链接。
7.1 创建链接文件
创建硬链接 link()
link()系统调用用于创建硬链接文件,函数原型如下(可通过”man 2 link”命令查看):
1
2
3
int link(const char *oldpath, const char *newpath);
oldpath: 用于指定被链接的源文件路径,应避免 oldpath 参数指定为软链接文件,为软链接文件创建硬链接没有意义,虽然并不会报错。
newpath: 用于指定硬链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误。
返回值: 成功返回 0;失败将返回-1,并且会设置 errno。
创建软链接 symlink()
symlink()系统调用用于创建软链接文件,函数原型如下(可通过”man 2 symlink”命令查看):
1
2
3
int symlink(const char *target, const char *linkpath);
- target: 用于指定被链接的源文件路径, target 参数指定的也可以是一个软链接文件。
- linkpath: 用于指定硬链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误。
- 返回值: 成功返回 0;失败将返回-1,并会设置 errno。
创建软链接时,并不要求target 参数指定的文件路径已经存在,如果文件不存在,那么创建的软链接将成为“悬空链接”。
7.2 读取链接文件
readlink 函数原型如下所示:
1
2
3
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
pathname: 需要读取的软链接文件路径。只能是软链接文件路径,不能是其它类型文件,否则调用函数将报错。
buf: 用于存放路径信息的缓冲区。
bufsiz: 读取大小,一般读取的大小需要大于链接文件数据块中存储的文件路径信息字节大小。
返回值:失败将返回-1,并会设置 errno;成功将返回读取到的字节数。
8 目录
目录(文件夹)在 Linux 系统也是一种文件,是一种特殊文件,同样可以使用前面给大家介绍 open、read 等这些系统调用以及 C 库函数对其进行操作,但是目录作为一种特殊文件,并不适合使用前面介绍的文件 I/O 方式进行读写等操作。在 Linux 系统下,会有一些专门的系统调用或 C 库函数用于对文件夹进行操作,譬如:打开、创建文件夹、删除文件夹、读取文件夹以及遍历文件夹中的文件等。
8.1 目录存储形式
对于目录来说,其存储形式则是由 inode 节点和目录块所构成,目录块当中记录了有哪些文件组织在这个目录下,记录它们的文件名以及对应的 inode 编号。
目录块当中有多个目录项(或叫目录条目),每一个目录项(或目录条目)都会对应到该目录下的某一个文件,目录项当中记录了该文件的文件名以及它的 inode 节点编号,所以通过目录的目录块便可以遍历找到该目录下的所有文件以及所对应的 inode 节点。
8.2 创建和删除目录
在 Linux 系统下,提供了专门用于创建目录 mkdir()以及删除目录 rmdir 相关的系统调用。
mkdir 函数原型如下所示:
1
2
3
4
int mkdir(const char *pathname, mode_t mode);
pathname: 需要创建的目录路径。
mode: 新建目录的权限设置,设置方式与 open 函数的 mode 参数一样,最终权限为(mode & ~umask)。
返回值: 成功返回 0;失败将返回-1,并会设置 errno。
pathname 参数指定的新建目录的路径,该路径名可以是相对路径,也可以是绝对路径,若指定的路径名已经存在, 则调用 mkdir()将会失败。
rmdir()用于删除一个目录
1
2
3
int rmdir(const char *pathname);
- pathname: 需要删除的目录对应的路径名,并且该目录必须是一个空目录,也就是该目录下只有.和..这两个目录项;pathname 指定的路径名不能是软链接文件,即使该链接文件指向了一个空目录。
- 返回值: 成功返回 0;失败将返回-1,并会设置 errno。
8.3 打开,读取及关闭目录
对于目录来说,可以使用 opendir()、readdir()和 closedir()来打开、读取以及关闭目录。
opendir()函数用于打开一个目录,并返回指向该目录的句柄,供后续操作使用。 Opendir 是一个 C 库函数, opendir()函数原型如下所示:
1
2
3
4
DIR *opendir(const char *name);
name: 指定需要打开的目录路径名,可以是绝对路径,也可以是相对路径。
返回值: 成功将返回指向该目录的句柄,一个DIR 指针(其实质是一个结构体指针),其作用类似于open函数返回的文件描述符fd,后续对该目录的操作需要使用该DIR指针变量;若调用失败,则返回NULL。
readdir()用于读取目录,获取目录下所有文件的名称以及对应 inode 号。其函数原型如下所示:
1
2
3
struct dirent *readdir(DIR *dirp);
dirp: 目录句柄 DIR 指针。
返回值: 返回一个指向 struct dirent 结构体的指针,该结构体表示 dirp 指向的目录流中的下一个目录条目。在到达目录流的末尾或发生错误时,它返回 NULL。
struct dirent 结构体内容如下所示
1
2
3
4
5
6
7 struct dirent {
ino_t d_ino; /* inode 编号 */
off_t d_off; /* not an offset; see NOTES */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported by all filesystem types */
char d_name[256]; /* 文件名 */
};对于 struct dirent 结构体,我们只需要关注 d_ino 和 d_name 两个字段即可,分别记录了文件的 inode 编号和文件名 。
rewinddir()是 C 库函数,可将目录流重置为目录起点,以便对 readdir()的下一次调用将从目录列表中的第一个文件开始。 rewinddir 函数原型如下所示:
1
2
3
void rewinddir(DIR *dirp);
dirp: 目录句柄。
返回值: 无返回值。
closedir()函数用于关闭处于打开状态的目录,同时释放它所使用的资源,其函数原型如下所示:
1
2
3
4
int closedir(DIR *dirp);
dirp: 目录句柄。
返回值: 成功返回0;失败将返回-1,并设置 errno。
8.4 进程的当前工作目录
一般情况下,运行一个进程时、其父进程的当前工作目录将被该进程所继承,成为该进程的当前工作目录。可通过 getcwd 函数来获取进程的当前工作目录,如下所示:
1
2
3
char *getcwd(char *buf, size_t size);
- buf: getcwd()将内含当前工作目录绝对路径的字符串存放在 buf 缓冲区中。
- size: 缓冲区的大小,分配的缓冲区大小必须要大于字符串长度,否则调用将会失败。
- 返回值: 如果调用成功将返回指向 buf 的指针,失败将返回 NULL,并设置 errno。
chdir()和 fchdir()可以用于更改进程的当前工作目录,函数原型如下所示:
1
2
3
4
int chdir(const char *path);
int fchdir(int fd);
path: 将进程的当前工作目录更改为 path 参数指定的目录,可以是绝对路径、也可以是相对路径,指定的目录必须要存在,否则会报错。
fd: 将进程的当前工作目录更改为 fd 文件描述符所指定的目录(譬如使用 open 函数打开一个目录)。
返回值: 成功均返回 0;失败均返回-1,并设置 errno。
此两函数的区别在于,指定目录的方式不同, chdir()是以路径的方式进行指定,而 fchdir()则是通过文件描述符,文件描述符可调用 open()打开相应的目录时获得。
9 删除文件
通过系统调用 unlink()或使用 C 库函数 remove()。
使用 unlink函数删除文件
1
2
3
int unlink(const char *pathname);
- pathname: 需要删除的文件路径,可使用相对路径、也可使用绝对路径,如果 pathname 参数指定的文件不存在,则调用 unlink()失败。
- 返回值: 成功返回 0;失败将返回-1,并设置 errno。
unlink()的作用与 link()相反, unlink()系统调用用于移除/删除一个硬链接(从其父级目录下删除该目录条目)。
使用 remove 函数删除文件
remove()是一个 C 库函数,用于移除一个文件或空目录,其函数原型如下所示:
1
2
3
int remove(const char *pathname);
- pathname: 需要删除的文件或目录路径,可以是相对路径、也可是决定路径。
- 返回值: 成功返回 0;失败将返回-1,并设置 errno。
pathname 参数指定的是一个非目录文件,那么 remove()去调用 unlink(),如果 pathname 参数指定的是一个目录,那么 remove()去调用 rmdir()。
与 unlink()、 rmdir()一样, remove()不对软链接进行解引用操作,若 pathname 参数指定的是一个软链接文件,则 remove()会删除链接文件本身、而非所指向的文件。
10 文件重命名
借助于 rename()既可以对文件进行重命名,又可以将文件移至同一文件系统中的另一个目录下,其函数原型如下所示:
1
2
3
int rename(const char *oldpath, const char *newpath);
oldpath: 原文件路径。
newpath: 新文件路径。
返回值: 成功返回0;失败将返回-1,并设置 errno。
调用 rename()会将现有的一个路径名 oldpath 重命名为 newpath 参数所指定的路径名。 rename()调用仅操作目录条目,而不移动文件数据(不改变文件 inode 编号、不移动文件数据块中存储的内容) ,重命名既不影响指向该文件的其它硬链接,也不影响已经打开该文件的进程(譬如,在重命名之前该文件已被其它进程打开了,而且还未被关闭)。