Linux_应用篇(05) 文件属性与目录

在前面的章节内容中,都是围绕普通文件 I/O 操作进行的一系列讨论,譬如打开文件、读写文件、关闭文件等,本章将抛开文件 I/O 相关话题,来讨论 Linux 文件系统的其它特性以及文件相关属性;我们将从系统调用 stat 开始,可利用其返回一个包含多种文件属性(包括文件时间戳、文件所有权以及文件权限等)的结构体,逐个说明 stat 结构中的每一个成员以了解文件的所有属性,然后将向大家介绍用以改变文件属性的各种系统调用;除此之外,还会向大家介绍 Linux 系统中的符号链接以及目录相关的操作。
本章将会讨论如下主题内容。
⚫ Linux 系统的文件类型;
⚫ stat 系统调用;
⚫ 文件各种属性介绍:文件属主、访问权限、时间戳;
⚫ 符号链接与硬链接;
⚫ 目录;
⚫ 删除文件与文件重命名。

Linux 系统中的文件类型

Linux 下一切皆文件,文件作为 Linux 系统设计思想的核心理念,在 Linux 系统下显得尤为重要。在前面章节内容中,我们都是以普通文件(文本文件、二进制文件等)为例来给大家讲解文件 I/O 相关的知识内容; 虽然在 Linux 系统中大部分文件都是普通文件,但并不仅仅只有普通文件,那么本小节将向大家介绍Linux 系统中的文件类型。
在 Windows 系统下,操作系统识别文件类型一般是通过文件名后缀来判断,譬如 C 语言头文件.h、 C语言源文件.c、 .txt 文本文件、 压缩包文件.zip 等,在 Windows 操作系统下打开文件,首先会识别文件名后缀得到该文件的类型,然后再使用相应的调用相应的程序去打开它;譬如.c 文件,则会使用 C 代码编辑器去打开它; .zip 文件,则会使用解压软件去打开它。但是在 Linux 系统下,并不会通过文件后缀名来识别一个文件的类型,话虽如此,但并不是意味着大家可以随便给文件加后缀;文件名也好、后缀也好都是给“人”看的, 虽然 Linux 系统并不会通过后缀来识别文件, 但是文件后缀也要规范、需要根据文件本身的功能属性来添加,譬如 C 源文件就以.c 为后缀、 C 头文件就以.h 为后缀、 shell 脚本文件就以.sh 为后缀、这是为了我们自己方便查看、浏览。Linux 系统下一共分为 7 种文件类型,下面依次给大家介绍。

普通文件

普通文件(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 ':管道文件
关于普通文件就给大家介绍这么多。

目录文件

目录(directory) 就是文件夹,文件夹在 Linux 系统中也是一种文件,是一种特殊文件,同样我们也可以使用 vi 编辑器来打开文件夹,如下所示:

可以看到,文件夹中记录了该文件夹本身的路径以及该文件夹下所存放的文件。文件夹作为一种特殊文件,本身并不适合使用前面给大家介绍的文件 I/O 的方式来读写,在 Linux 系统下,会有一些专门的系统调用用于读写文件夹,这部分内容后面再给大家介绍。

字符设备文件和块设备文件

学过 Linux 驱动编程开发的读者,对字符设备文件(character) 、块设备文件(block) 这些文件类型应该并不陌生, Linux 系统下,一切皆文件,也包括各种硬件设备。 设备文件(字符设备文件、块设备文件)对应的是硬件设备,在 Linux 系统中,硬件设备会对应到一个设备文件,应用程序通过对设备文件的读写来操控、使用硬件设备,譬如 LCD 显示屏、串口、音频、按键等,在本教程的进阶篇内容中,将会向大家介绍如何通过设备文件操控、使用硬件设备。Linux 系统中,可将硬件设备分为字符设备和块设备,所以就有了字符设备文件和块设备文件两种文件类型。虽然有设备文件,但是设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护, 当系统关机时,设备文件都会消失; 字符设备文件一般存放在 Linux 系统/dev/目录下,所以/dev 也称为虚拟文件系统 devfs。以 Ubuntu 系统为例,如下所示:

上图中 agpgart、 autofs、 btrfs-control、 console 等这些都是字符设备文件,而 loop0、 loop1 这些便是块设备文件。

符号链接文件

符号链接文件(link) 类似于 Windows 系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另一个文件路径,当对符号链接文件进行操作时,系统根据情况会对这个操作转移到它指向的文件上去,而不是对它本身进行操作,譬如,读取一个符号链接文件内容时,实际上读到的是它指向的文件的内容。如果大家理解了 Windows 下的快捷方式,那么就会很容易理解 Linux 下的符号链接文件。 上图中的 cdrom、 cdrw、 fd、 initctl 等这些文件都是符号链接文件,箭头所指向的文件路径便是符号链接文件所指向的文件。
关于链接文件,在后面的内容中还会给大家进行介绍,这里暂时给大家介绍这么多!

管道文件

管道文件(pipe) 主要用于进程间通信,当学习到相关知识内容的时候再给大家详解。

套接字文件

套接字文件(socket)也是一种进程间通信的方式,与管道文件不同的是,它们可以在不同主机上的进程间通信,实际上就是网络通信,当学习到网络编程相关知识内容再给大家介绍。

stat 函数

Linux 下可以使用 stat 命令查看文件的属性,其实这个命令内部就是通过调用 stat()函数来获取文件属性的, stat 函数是 Linux 中的系统调用,用于获取文件相关的信息,函数原型如下所示(可通过"man 2 stat"命令查看) :

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *buf);

首先使用该函数需要包含<sys/types.h>、 <sys/stat.h>以及<unistd.h>这三个头文件。
函数参数及返回值含义如下:
pathname: 用于指定一个需要查看属性的文件路径。
buf: struct stat 类型指针,用于指向一个 struct stat 结构体变量。调用 stat 函数的时候需要传入一个 structstat 变量的指针, 获取到的文件属性信息就记录在 struct stat 结构体中,稍后给大家介绍 struct stat 结构体中有记录了哪些信息。
返回值: 成功返回 0;失败返回-1,并设置 error。

struct stat 结构体

struct stat 是内核定义的一个结构体,在<sys/stat.h>头文件中申明,所以可以在应用层使用,这个结构体中的所有元素加起来构成了文件的属性信息,结构体内容如下所示:

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 类型变量

st_mode 变量

st_mode 是 struct stat 结构体中的一个成员变量, 是一个 32 位无符号整形数据,该变量记录了文件的类型、文件的权限这些信息,其表示方法如下所示:

看到图的时候,大家有没有似曾相识的感觉,确实,前面章节内容给大家介绍 open 函数的第三个参数 mode 时也用到了类似的图,唯一不同的在于 open 函数的 mode 参数只涉及到 S、U、 G、 O 这 12 个 bit 位, 并不包括用于描述文件类型的 4 个 bit 位。
O 对应的 3 个 bit 位用于描述其它用户的权限;
G 对应的 3 个 bit 位用于描述同组用户的权限;
U 对应的 3 个 bit 位用于描述文件所有者的权限;
S 对应的 3 个 bit 位用于描述文件的特殊权限。
这些 bit 位表达内容与 open 函数的 mode 参数相对应,这里不再重述。同样, 在 mode 参数中表示权限的宏定义,在这里也是可以使用的, 这些宏定义如下(以下数字使用的是八进制方式表示) :

S_IRWXU 00700 owner has read, write, and execute permission
S_IRUSR 00400 owner has read permission
S_IWUSR 00200 owner has write permission
S_IXUSR 00100 owner has execute permission
S_IRWXG 00070 group has read, write, and execute permission
S_IRGRP 00040 group has read permission
S_IWGRP 00020 group has write permission
S_IXGRP 00010 group has execute permission
S_IRWXO 00007 others (not in group) have read, write, and execute permission
S_IROTH 00004 others have read permission
S_IWOTH 00002 others have write permission
S_IXOTH 00001 others have execute permission

譬如,判断文件所有者对该文件是否具有可执行权限,可以通过以下方法测试(假设 st 是 struct stat 类型变量) :

if (st.st_mode & S_IXUSR) {
    //有权限
} else {
    //无权限
}

这里我们重点来看看“文件类型”这 4 个 bit 位, 这 4 个 bit 位用于描述该文件的类型,譬如该文件是普通文件、还是链接文件、亦或者是一个目录等,那么就可以通过这 4 个 bit 位数据判断出来,如下所示:

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(管道文件)

注意上面这些数字使用的是八进制方式来表示的,在 C 语言中,八进制方式表示一个数字需要在数字前面添加一个 0(零) 。所以由上面可知,当“文件类型”这 4 个 bit 位对应的数字是 14(八进制)时,表示该文件是一个套接字文件、当“文件类型”这 4 个 bit 位对应的数字是 12(八进制)时,表示该文件是一个链接文件、当“文件类型”这 4 个 bit 位对应的数字是 10(八进制)时,表示该文件是一个普通文件等。所以通过 st_mode 变量判断文件类型就很简单了,如下(假设 st 是 struct stat 类型变量) :

/* 判断是不是普通文件 */
if ((st.st_mode & S_IFMT) == S_IFREG) {
    /* 是 */
}

/* 判断是不是链接文件 */
if ((st.st_mode & S_IFMT) == S_IFLNK) {
    /* 是 */
}

S_IFMT 宏是文件类型字段位掩码:
S_IFMT 0170000

除了这样判断之外,我们还可以使用 Linux 系统封装好的宏来进行判断,如下所示(m 是 st_mode 变量) :

S_ISREG(m) #判断是不是普通文件,如果是返回 true,否则返回 false
S_ISDIR(m) #判断是不是目录,如果是返回 true,否则返回 false
S_ISCHR(m) #判断是不是字符设备文件,如果是返回 true,否则返回 false
S_ISBLK(m) #判断是不是块设备文件,如果是返回 true,否则返回 false
S_ISFIFO(m) #判断是不是管道文件,如果是返回 true,否则返回 false
S_ISLNK(m) #判断是不是链接文件,如果是返回 true,否则返回 false
S_ISSOCK(m) #判断是不是套接字文件,如果是返回 true,否则返回 false

有了这些宏之后,就可以通过如下方式来判断文件类型了:

/* 判断是不是普通文件 */
if (S_ISREG(st.st_mode)) {
    /* 是 */
}

/* 判断是不是目录 */
if (S_ISDIR(st.st_mode)) {
    /* 是 */
}

struct timespec 结构体

该结构体定义在<time.h>头文件中, 是 Linux 系统中时间相关的结构体。应用程序中包含了<time.h>头文件, 就可以在应用程序中使用该结构体了,结构体内容如下所示:

struct timespec
{
    time_t tv_sec; /* 秒 */
    syscall_slong_t tv_nsec; /* 纳秒 */
};

struct timespec 结构体中只有两个成员变量,一个秒(tv_sec) 、一个纳秒(tv_nsec), time_t 其实指的就是 long int 类型, 所以由此可知,该结构体所表示的时间可以精确到纳秒,当然,对于文件的时间属性来说,并不需要这么高的精度,往往只需精确到秒级别即可。
在 Linux 系统中, time_t 时间指的是一个时间段,从某一个时间点到某一个时间点所经过的秒数, 譬如对于文件的三个时间属性来说,指的是从过去的某一个时间点(这个时间点是一个起始基准时间点)到文件最后被访问、 文件内容最后被修改、 文件状态最后被改变的这个时间点所经过的秒数。 time_t 时间在 Linux下被称为日历时间。struct stat 结构体中包含了三个文件相关的时间属性,但这里得到的仅仅只是以秒+微秒为单位的时间值,对于我们来说,并不方便查看,我们一般喜欢的是“2020-10-10 18:30:30”这种形式表示的时间,直观、明了,那有没有办法通过秒来得到这种形式表达的时间呢?答案当然是可以, 譬如可以通过 localtime()/localtime_r()或者 strftime()来得到更利于我们查看的时间表达方式。

fstat 和 lstat 函数

前面给大家介绍了 stat 系统调用,除了 stat 函数之外,还可以使用 fstat 和 lstat 两个系统调用来获取文件属性信息。 fstat、 lstat 与 stat 的作用一样,但是参数、细节方面有些许不同。

fstat 函数

fstat 与 stat 区别在于, stat 是从文件名出发得到文件属性信息,不需要先打开文件;而 fstat 函数则是从文件描述符出发得到文件属性信息,所以使用 fstat 函数之前需要先打开文件得到文件描述符。具体该用 stat还是 fstat,看具体的情况;譬如,并不想通过打开文件来得到文件属性信息,那么就使用 stat,如果文件已经打开了,那么就使用 fstat。
fstat 函数原型如下(可通过"man 2 fstat"命令查看):

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int fstat(int fd, struct stat *buf);

第一个参数 fd 表示文件描述符,第二个参数以及返回值与 stat 一样。 fstat 函数使用示例如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    struct stat file_stat;
    int fd;
    int ret;

    /* 打开文件 */
    fd = open("./test_file", O_RDONLY);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }

    /* 获取文件属性 */
    ret = fstat(fd, &file_stat);
    if (-1 == ret)
        perror("fstat error");

    close(fd);
    exit(ret);
}

lstat 函数

lstat()与 stat、 fstat 的区别在于,对于符号链接文件, stat、 fstat 查阅的是符号链接文件所指向的文件对应的文件属性信息,而 lstat 查阅的是符号链接文件本身的属性信息。
lstat 函数原型如下所示:

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int lstat(const char *pathname, struct stat *buf);

函数参数列表、返回值与 stat 函数一样,使用方法也一样,这里不再重述!

文件属主

Linux 是一个多用户操作系统, 系统中一般存在着好几个不同的用户,而 Linux 系统中的每一个文件都有一个与之相关联的用户和用户组, 通过这个信息可以判断文件的所有者和所属组。文件所有者表示该文件属于“谁”,也就是属于哪个用户。一般来说文件在创建时,其所有者就是创建该文件的那个用户。 譬如,当前登录用户为 dt,使用 touch 命令创建了一个文件,那么这个文件的所有者就是 dt;同理,在程序中调用 open 函数创建新文件时也是如此,执行该程序的用户是谁,其文件所有者便是谁。文件所属组则表示该文件属于哪一个用户组。在 Linux 中,系统并不是通过用户名或用户组名来识别不同的用户和用户组,而是通过 ID。 ID 就是一个编号, Linux 系统会为每一个用户或用户组分配一个 ID, 将用户名或用户组名与对应的 ID 关联起来, 所以系统通过用户 ID(UID) 或组 ID(GID) 就可以识别出不同的用户和用户组。譬如使用 ls 命令或 stat 命令便可以查看到文件的所有者和所属组,如下所示:

由上图可知, testApp.c 文件的用户 ID 是 1000,用户组 ID 也是 1000。
文件的用户 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 是站在操作系统的角度,用于给操作系统判断当前执行该进程的用户在当前环境下对某个文件是否拥有相应的权限。在 Linux 系统中,当进程对文件进行读写操作时,系统首先会判断该进程是否具有对该文件的读写权限,那如何判断呢?自然是通过该文件的权限位来判断, struct stat 结构体中的 st_mode 字段中就记录了该文件的权限位以及文件类型。当进行权限检查时,并不是通过进程的实际用户和实际组来参与权限检查的,而是通过有效用户和有效组来参与文件权限检查。 通常, 绝大部分情况下,进程的有效用户等于实际用户(有效用户 ID 等于实际用户 ID) ,有效组等于实际组(有效组 ID 等于实际组 ID) 。那么大家可能就要问了,什么情况下有效用户 ID 不等于实际用户 ID、有效组 ID 不等于实际组 ID?那么关于这个问题,后面将给大家揭晓!

chown 函数

chown 是一个系统调用,该系统调用可用于改变文件的所有者(用户 ID)和所属组(组 ID) 。其实在Linux 系统下也有一个 chown 命令,该命令的作用也是用于改变文件的所有者和所属组,譬如将 testApp.c文件的所有者和所属组修改为 root:

可以看到,通过该命令确实可以改变文件的所有者和所属组,这个命令内部其实就是调用了 chown 函数来实现功能的, chown 函数原型如下所示(可通过"man 2 chown"命令查看):

#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);

首先,使用该命令需要包含头文件<unistd.h>。
函数参数和返回值如下所示:
pathname: 用于指定一个需要修改所有者和所属组的文件路径。
owner: 将文件的所有者修改为该参数指定的用户(以用户 ID 的形式描述) ;
group: 将文件的所属组修改为该参数指定的用户组(以用户组 ID 的形式描述);
返回值: 成功返回 0;失败将返回-1,兵并且会设置 errno。
该函数的用法非常简单,只需指定对应的文件路径以及相应的 owner 和 group 参数即可!如果只需要修改文件的用户 ID 和用户组 ID 当中的一个,那么又该如何做呢?方法很简单,只需将其中不用修改的 ID(用户 ID 或用户组 ID)与文件当前的 ID(用户 ID 或用户组 ID)保持一致即可,即调用 chown 函数时传入的用户 ID 或用户组 ID 就是该文件当前的用户 ID 或用户组 ID,而文件当前的用户 ID 或用户组 ID 可以通过stat 函数查询获取。
虽然该函数用法很简单,但是有以下两个限制条件:
⚫ 只有超级用户进程能更改文件的用户 ID;
⚫ 普通用户进程可以将文件的组 ID 修改为其所从属的任意附属组 ID,前提条件是该进程的有效用户 ID 等于文件的用户 ID;而超级用户进程可以将文件的组 ID 修改为任意值。
所以,由此可知,文件的用户 ID 和组 ID 并不是随随便便就可以更改的,其实这种设计是为系统安全着想,如果系统中的任何普通用户进程都可以随便更改系统文件的用户 ID 和组 ID,那么也就意味着任何普通用户对系统文件都有任意权限了,这对于操作系统来说将是非常不安全的。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    if (-1 == chown("./test_file", 0, 0)) {
        perror("chown error");
        exit(-1);
    }

    exit(0);
}

代码很简单,直接调用 chown 函数将 test_file 文件的用户 ID 和用户组 ID 修改为 0、 0。 0 指的就是 root用户和 root 用户组,接下来我们测试下:

在运行测试代码之前,先使用了 stat 命令查看到 test_file 文件的用户 ID 和用户组 ID 都等于 1000,然后执行测试程序,结果报错"Operation not permitted",显示不允许操作;接下来重新执行程序,此时加上 sudo,如下:

此时便可以看到,执行之后没有打印错误提示信息,说明 chown 函数调用成功了, 并且通过 stat 命令也可以看到文件的用户 ID 和组 ID 确实都被修改为 0 了(也就是 root 用户) 。 原因在于,加上 sudo 执行应用程序,而此时应用程序便可以临时获得 root 用户的权限,也就是会以 root 用户的身份运行程序,也就意味着此时该应用程序的用户 ID(也就是前面给大家提到的实际用户 ID) 变成了 root 超级用户的 ID(也就是 0),自然 chown 函数便可以调用成功。
在 Linux 系统下,可以使用 getuid 和 getgid 两个系统调用分别用于获取当前进程的用户 ID 和用户组ID,这里说的进程的用户 ID 和用户组 ID 指的就是进程的实际用户 ID 和实际组 ID,这两个系统调用函数原型如下所示:

#include <unistd.h>
#include <sys/types.h>
uid_t getuid(void);
gid_t getgid(void);

fchown 和 lchown 函数

这两个同样也是系统调用,作用与 chown 函数相同,只是参数、细节方面有些许不同。 fchown()、 lchown()这两个函数与 chown()的区别就像是 fstat()、 lstat()与 stat 的区别,本小节就不再重述这种问题了。

文件访问权限

struct stat 结构体中的 st_mode 字段记录了文件的访问权限位。当提及到文件时,指的是前面给大家介绍的任何类型的文件,并不仅仅指的是普通文件;所有文件类型(目录、设备文件)都有访问权限(access permission),可能有很多人认为只有普通文件才有访问权限,这是一种误解!

普通权限和特殊权限

文件的权限可以分为两个大类,分别是普通权限和特殊权限(也可称为附加权限)。普通权限包括对文件的读、写以及执行,而特殊权限则包括一些对文件的附加权限,譬如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 个访问权限,如下所示:

最前面的一个字符表示该文件的类型,这个前面给大家介绍过, " - "表示该文件是一个普通文件。
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 三个宏来表示:

S_ISUID 04000 set-user-ID bit
S_ISGID 02000 set-group-ID bit (see below)
S_ISVTX 01000 sticky bit (see below)

同样,以上数字使用的是八进制方式表示。 对应的 bit 位数字为 1,则表示设置了该权限、为 0 则表示并未设置该权限; 譬如通过 st_mode 变量判断文件是否设置了 set-user-ID 位权限,代码如下:

if (st.st_mode & S_ISUID) {
    //设置了 set-user-ID 位权限
} else {
    //没有设置 set-user-ID 位权限
}

这三个权限位具体有什么作用呢? 接下里给大家简单地介绍一下:
⚫ 当进程对文件进行操作的时候、将进行权限检查,如果文件的 set-user-ID 位权限被设置,内核会将进程的有效 ID 设置为该文件的用户 ID(文件所有者 ID) ,意味着该进程直接获取了文件所有者的权限、以文件所有者的身份操作该文件。
⚫ 当进程对文件进行操作的时候、将进行权限检查,如果文件的 set-group-ID 位权限被设置,内核会将进程的有效用户组 ID 设置为该文件的用户组 ID(文件所属组 ID) ,意味着该进程直接获取了文件所属组成员的权限、以文件所属组成员的身份操作该文件。
当然, set-user-ID 位和 set-group-ID 位权限的作用并不如此简单,关于其它的功能本文档便不再叙述了,因为这些特殊权限位实际中用到的机会确实不多。除此之外, Sticky 位权限也不再给大家介绍了,笔者对此也不是很了解,有兴趣的读者可以自行查阅相关的书籍。Linux 系统下绝大部分的文件都没有设置 set-user-ID 位权限和 set-group-ID 位权限,所以通常情况下,进程的有效用户等于实际用户(有效用户 ID 等于实际用户 ID),有效组等于实际组(有效组 ID 等于实际组 ID)。

目录权限

前面我们一直谈论的都是文件的读、写、执行权限,那对于创建文件、删除文件等这些操作难道就不需要相应的权限了吗?事实并不如此,譬如:有时删除文件或创建文件也会提示"权限不够",如下所示:

那说明删除文件、创建文件这些操作也是需要相应权限的,那这些权限又是从哪里获取的呢?答案就是目录。 目录(文件夹)在 Linux 系统下也是一种文件,拥有与普通文件相同的权限方案(S/U/G/O) ,只是这些权限的含义另有所指。
⚫ 目录的读权限: 可列出(譬如:通过 ls 命令) 目录之下的内容(即目录下有哪些文件)。
⚫ 目录的写权限: 可以在目录下创建文件、删除文件。
⚫ 目录的执行权限: 可访问目录下的文件,譬如对目录下的文件进行读、写、执行等操作。
拥有对目录的读权限,用户只能查看目录中的文件列表,譬如使用 ls 命令进行查看:

通过"ls -l"命令可以查看到 2_chapter 目录对于文件所有者只有读权限,当前操作的用户正是该目录所有者 dt,之后通过"ls 2_chapter"命令查看该目录下的文件,确实获取到了该目录下的 3 个文件: file1、 file2、file3,说明只有读权限时,可以查看到目录下有哪些文件、显示出文件的名称; 但是会看到上面打印出了一些"权限不够"信息,这是因为 Ubuntu 发行版对 ls 命令做了别名处理,执行 ls 命令的时候携带了一些选项,而这些选项会访问文件的一些信息,所以导致出现"权限不够"问题,这也说明,只拥有读权限、是没法访问目录下的文件的;为了确保使用的是 ls 命令本身,执行时需要给出路径的完整路径/bin/ls:

要想访问目录下的文件,譬如查看文件的 inode 节点、大小、权限等信息,还需要对目录拥有执行权限。反之,若拥有对目录的执行权限、而无读权限,只要知道目录内文件的名称,仍可对其进行访问,但不能列出目录下的内容(即目录下包含的其它文件的名称)。要想在目录下创建文件或删除原有文件,需要同时拥有对该目录的执行和写权限。所以由此可知,如果需要对文件进行读、写或执行等操作,不光是需要拥有该文件本身的读、写或执行权限,还需要拥有文件所在目录的执行权限。

检查文件权限 access

通过前面的介绍,大家应该知道了,文件的权限检查不单单只讨论文件本身的权限,还需要涉及到文件所在目录的权限, 只有同时都满足了,才能通过操作系统的权限检查,进而才可以对文件进行相关操作;所以,程序当中对文件进行相关操作之前,需要先检查执行进程的用户是否对该文件拥有相应的权限。那如何检查呢?可以使用 access 系统调用,函数原型如下:

#include <unistd.h>
int access(const char *pathname, int mode);

首先,使用该函数需要包含头文件<unistd.h>。
函数参数和返回值含义如下:
pathname: 需要进行权限检查的文件路径。
mode: 该参数可以取以下值:
⚫ F_OK:检查文件是否存在
⚫ R_OK:检查是否拥有读权限
⚫ W_OK:检查是否拥有写权限
⚫ X_OK:检查是否拥有执行权限
除了可以单独使用之外,还可以通过按位或运算符" | "组合在一起。
返回值: 检查项通过则返回 0,表示拥有相应的权限并且文件存在;否则返回-1,如果多个检查项组合在一起,只要其中任何一项不通过都会返回-1。

修改文件权限 chmod

在 Linux 系统下,可以使用 chmod 命令修改文件权限,该命令内部实现方法其实是调用了 chmod 函数,chmod 函数是一个系统调用,函数原型如下所示(可通过"man 2 chmod"命令查看):

#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);

首先,使用该函数需要包含头文件<sys/stat.h>。
函数参数及返回值如下所示:
pathname: 需要进行权限修改的文件路径,若该参数所指为符号链接,实际改变权限的文件是符号链接所指向的文件,而不是符号链接文件本身。
mode: 该参数用于描述文件权限,与 open 函数的第三个参数一样,这里不再重述,可以直接使用八进制数据来描述,也可以使用相应的权限宏(单个或通过位或运算符" | "组合)。
返回值: 成功返回 0;失败返回-1,并设置 errno。
文件权限对于文件来说是非常重要的属性,是不能随随便便被任何用户所修改的, 要想更改文件权限,要么是超级用户(root) 进程、要么进程有效用户 ID 与文件的用户 ID(文件所有者)相匹配。

fchmod 函数
该函数功能与 chmod 一样,参数略有不同。 fchmod()与 chmod()的区别在于使用了文件描述符来代替文件路径,就像是 fstat 与 stat 的区别。函数原型如下所示:

#include <sys/stat.h>
int fchmod(int fd, mode_t mode);

使用了文件描述符 fd 代替了文件路径 pathname,其它功能都是一样的。

umask 函数
在 Linux 下有一个 umask 命令,在 Ubuntu 系统下执行看看:


可以看到该命令打印出了"0002",这数字表示什么意思呢? 这就要从 umask 命令的作用说起了, umask命令用于查看/设置权限掩码, 权限掩码主要用于对新建文件的权限进行屏蔽。权限掩码的表示方式与文件权限的表示方式相同, 但是需要去除特殊权限位, umask 不能对特殊权限位进行屏蔽。当新建文件时,文件实际的权限并不等于我们所设置的权限,譬如:调用 open 函数新建文件时,文件实际的权限并不等于 mode 参数所描述的权限,而是通过如下关系得到实际权限:

mode & ~umask

譬如调用 open 函数新建文件时, mode 参数指定为 0777, 假设 umask 为 0002,那么实际权限为:

0777 & (~0002) = 0775

前面给大家介绍 open 函数的 mode 参数时,并未向大家提及到 umask,所以这里重新向大家说明。
umask 权限掩码是进程的一种属性,用于指明该进程新建文件或目录时,应屏蔽哪些权限位。 进程的umask 通常继承至其父进程(关于父、 子进程相关的内容将会在后面章节给大家介绍) ,譬如在 Ubuntu shell终端下执行的应用程序,它的 umask 继承至该 shell 进程。当然, Linux 系统提供了 umask 函数用于设置进程的权限掩码,该函数是一个系统调用,函数原型如下所示(可通过"man 2 umask"命令查看):

#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);

首先,使用该命令需要包含头文件<sys/types.h>和<sys/stat.h>。
函数参数和返回值含义如下:
mask: 需要设置的权限掩码值,可以发现 mask 参数的类型与 open 函数、 chmod 函数中的 mode 参数对应的类型一样,所以其表示方式也是一样的,前面也给大家介绍了,既可以使用数字表示(譬如八进制数)也可以直接使用宏(S_IRUSR、 S_IWUSR 等)。
返回值: 返回设置之前的 umask 值,也就是旧的 umask。

文件的时间属性

前面给大家介绍了 3 个文件的时间属性: 文件最后被访问的时间、 文件内容最后被修改的时间以及文件状态最后被改变的时间,分别记录在 struct stat 结构体的 st_atim、 st_mtim 以及 st_ctim 变量中,如下所示:

字段 说明
st_atim 文件最后被访问的时间
st_mtim 文件内容最后被修改的时间
st_ctim 文件状态最后被改变的时间

⚫ 文件最后被访问的时间: 访问指的是读取文件内容,文件内容最后一次被读取的时间,譬如使用read()函数读取文件内容便会改变该时间属性;
⚫ 文件内容最后被修改的时间: 文件内容发生改变,譬如使用 write()函数写入数据到文件中便会改变该时间属性;
⚫ 文件状态最后被改变的时间: 状态更改指的是该文件的 inode 节点最后一次被修改的时间,譬如更改文件的访问权限、更改文件的用户 ID、用户组 ID、更改链接数等,但它们并没有更改文件的实际内容,也没有访问(读取)文件内容。 为什么文件状态的更改指的是 inode 节点的更改呢? inode 中包含了很多文件信息,譬如:文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(时间属性)、文件数据存储的 block(块)等,所以由此可知,状态的更改指的就是 inode 节点内容的更改。譬如 chmod()、 chown()等这些函数都能改变该时间属性。
下表列出了一些系统调用或 C 库函数对文件时间属性的影响,有些操作并不仅仅只会影响文件本身的时间属性,还会影响到其父目录的相关时间属性。

utime()、 utimes()修改时间属性

文件的时间属性虽然会在我们对文件进行相关操作(譬如:读、写)的时候发生改变,但这些改变都是隐式、被动的发生改变,除此之外,还可以使用 Linux 系统提供的系统调用显式的修改文件的时间属性。本小节给大家介绍如何使用 utime()和 utimes()函数来修改文件的时间属性。
utime()函数
utime()函数原型如下所示:

#include <sys/types.h>
#include <utime.h>
int utime(const char *filename, const struct utimbuf *times);

首先,使用该函数需要包含头文件<sys/types.h>和<utime.h>。
函数参数和返回值含义如下:
filename: 需要修改时间属性的文件路径。
times: 将时间属性修改为该参数所指定的时间值, times 是一个 struct utimbuf 结构体类型的指针,稍后给大家介绍,如果将 times 参数设置为 NULL,则会将文件的访问时间和修改时间设置为系统当前时间。
返回值: 成功返回值 0;失败将返回-1,并会设置 errno。

来看看 struct utimbuf 结构体:

struct utimbuf {
    time_t actime; /* 访问时间 */
    time_t modtime; /* 内容修改时间 */
};

该结构体中包含了两个 time_t 类型的成员,分别用于表示访问时间和内容修改时间, time_t 类型其实就是 long int 类型,所以这两个时间是以秒为单位的,所以由此可知, utime()函数设置文件的时间属性精度只能到秒。同样对于文件来说,时间属性也是文件非常重要的属性之一,对文件时间属性的修改也不是任何用户都可以随便修改的, 只有以下两种进程可对其进行修改:
⚫ 超级用户进程(以 root 身份运行的进程) 。
⚫ 有效用户 ID 与该文件用户 ID(文件所有者)相匹配的进程。
⚫ 在参数 times 等于 NULL 的情况下,对文件拥有写权限的进程。
除以上三种情况之外的用户进程将无法对文件时间戳进行修改。

utime 测试

#include <sys/types.h>
#include <utime.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MY_FILE "./test_file"

int main(void)
{
    struct utimbuf utm_buf;
    time_t cur_sec;
    int ret;

    /* 检查文件是否存在 */
    ret = access(MY_FILE, F_OK);
    if (-1 == ret) {
        printf("Error: %s file does not exist!\n", MY_FILE);
        exit(-1);
    }

    /* 获取当前时间 */
    time(&cur_sec);
    utm_buf.actime = cur_sec;
    utm_buf.modtime = cur_sec;

    /* 修改文件时间戳 */
    ret = utime(MY_FILE, &utm_buf);
    if (-1 == ret) {
        perror("utime error");
        exit(-1);
    }

    exit(0);
}

上述代码尝试将 test_file 文件的访问时间和内容修改时间修改为当前系统时间。程序中使用到了 time()函数, time()是 Linux 系统调用,用于获取当前时间(也可以直接将 times 参数设置为 NULL,这样就不需要使用 time 函数来获取当前时间了), 单位为秒, 关于该函数在后面的章节内容中会给大家介绍,这里简单地了解一下。 接下来编译测试,在运行程序之间,先使用 stat 命令查看 test_file 文件的时间戳,如下:

接下来编译程序、运行测试:

会发现执行完测试程序之后, test_file 文件的访问时间和内容修改时间均被更改为当前时间了(大家可以使用 date 命令查看当前系统时间),并且会发现状态更改时间也会修改为当前时间了,当然这个不是在程序中修改、而是内核帮它自动修改的,为什么会这样呢?如果大家理解了之前介绍的知识内容,完全可以理解这个问题,这里笔者不再重述!

utimes()函数

utimes()也是系统调用,功能与 utime()函数一致,只是参数、细节上有些许不同, utimes()与 utime()最大的区别在于前者可以以微秒级精度来指定时间值,其函数原型如下所示:

#include <sys/time.h>
int utimes(const char *filename, const struct timeval times[2]);

首先,使用该函数需要包含头文件<sys/time.h>。
函数参数和返回值含义如下:
filename: 需要修改时间属性的文件路径。
times: 将时间属性修改为该参数所指定的时间值, times 是一个 struct timeval 结构体类型的数组, 数组共有两个元素, 第一个元素用于指定访问时间,第二个元素用于指定内容修改时间, 稍后给大家介绍,如果times 参数为 NULL,则会将文件的访问时间和修改时间设置为当前时间。
返回值: 成功返回 0;失败返回-1,并且会设置 errno。
来看看 struct timeval 结构体:

struct timeval {
    long tv_sec; /* 秒 */
    long tv_usec; /* 微秒 */
};

该结构体包含了两个成员变量 tv_sec 和 tv_usec,分别用于表示秒和微秒。
utimes()遵循与 utime()相同的时间戳修改权限规则。

futimens()、 utimensat()修改时间属性

除了上面给大家介绍了两个系统调用外,这里再向大家介绍两个系统调用,功能与 utime()和 utimes()函数功能一样,用于显式修改文件时间戳,它们是 futimens()和 utimensat()。
这两个系统调用相对于 utime 和 utimes 函数有以下三个优点:
⚫ 可按纳秒级精度设置时间戳。 相对于提供微秒级精度的 utimes(),这是重大改进!
⚫ 可单独设置某一时间戳。譬如,只设置访问时间、而修改时间保持不变,如果要使用 utime()或 utimes()来实现此功能,则需要首先使用 stat()获取另一个时间戳的值,然后再将获取值与打算变更的时间戳一同指定。
⚫ 可独立将任一时间戳设置为当前时间。使用 utime()或 utimes()函数虽然也可以通过将 times 参数设置为 NULL 来达到将时间戳设置为当前时间的效果,但是不能单独指定某一个时间戳,必须全部设置为当前时间(不考虑使用额外函数获取当前时间的方式,譬如 time())。
futimens()函数
futimens 函数原型如下所示(可通过"man 2 utimensat"命令查看):

#include <fcntl.h>
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);

函数原型和返回值含义如下:
fd: 文件描述符。
times: 将时间属性修改为该参数所指定的时间值, times 指向拥有 2 个 struct timespec 结构体类型变量的数组,数组共有两个元素, 第一个元素用于指定访问时间,第二个元素用于指定内容修改时间
返回值: 成功返回 0;失败将返回-1,并设置 errno。
所以由此可知,使用 futimens()设置文件时间戳,需要先打开文件获取到文件描述符。
该函数的时间戳可以按下列 4 种方式之一进行指定:
⚫ 如果 times 参数是一个空指针,也就是 NULL,则表示将访问时间和修改时间都设置为当前时间。
⚫ 如果 times 参数指向两个 struct timespec 结构体类型变量的数组,任一数组元素的 tv_nsec 字段的值设置为 UTIME_NOW,则表示相应的时间戳设置为当前时间,此时忽略相应的 tv_sec 字段。
⚫ 如果 times 参数指向两个 struct timespec 结构体类型变量的数组,任一数组元素的 tv_nsec 字段的值设置为 UTIME_OMIT,则表示相应的时间戳保持不变,此时忽略 tv_sec 字段。
⚫ 如果 times 参数指向两个 struct timespec 结构体类型变量的数组,且 tv_nsec 字段的值既不是UTIME_NOW 也不是 UTIME_OMIT,在这种情况下,相应的时间戳设置为相应的 tv_sec 和 tv_nsec字段指定的值。
使用 futimens()函数只有以下进程,可对文件时间戳进行修改:
⚫ 超级用户进程。
⚫ 在参数 times 等于 NULL 的情况下,对文件拥有写权限的进程。
⚫ 有效用户 ID 与该文件用户 ID(文件所有者)相匹配的进程。

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>

#define MY_FILE "./test_file"

int main(void)
{
    struct timespec tmsp_arr[2];
    int ret;
    int fd;

    /* 检查文件是否存在 */
    ret = access(MY_FILE, F_OK);
    if (-1 == ret) {
        printf("Error: %s file does not exist!\n", MY_FILE);
        exit(-1);
    }

    /* 打开文件 */
    fd = open(MY_FILE, O_RDONLY);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }

    /* 修改文件时间戳 */
    #if 1
        ret = futimens(fd, NULL); //同时设置为当前时间
    #endif

    #if 0
        tmsp_arr[0].tv_nsec = UTIME_OMIT;//访问时间保持不变
        tmsp_arr[1].tv_nsec = UTIME_NOW;//内容修改时间设置为当期时间
        ret = futimens(fd, tmsp_arr);
    #endif

    #if 0
        tmsp_arr[0].tv_nsec = UTIME_NOW;//访问时间设置为当前时间
        tmsp_arr[1].tv_nsec = UTIME_OMIT;//内容修改时间保持不变
        ret = futimens(fd, tmsp_arr);
    #endif

    if (-1 == ret) {
        perror("futimens error");
        goto err;
    }

err:
    close(fd);
    exit(ret);
}

utimensat()函数
utimensat()与 futimens()函数在功能上是一样的,同样可以实现纳秒级精度设置时间戳、单独设置某一时间戳、独立将任一时间戳设置为当前时间, 与 futimens()在参数以及细节上存在一些差异, 使用 futimens()函数,需要先将文件打开,通过文件描述符进行操作, utimensat()可以直接使用文件路径方式进行操作。
utimensat 函数原型如下所示:

#include <fcntl.h>
#include <sys/stat.h>
int utimensat(int dirfd, const char *pathname, const struct timespec times[2], int flags);

首先,使用该函数需要包含头文件<fcntl.h>和<sys/stat.h>。
函数参数和返回值含义如下:
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、并会设置时间戳。
utimensat()遵循与 futimens()相同的时间戳修改权限规则。

符号链接(软链接)与硬链接

在 Linux 系统中有两种链接文件,分为软链接(也叫符号链接)文件和硬链接文件,软链接文件也就是前面给大家的 Linux 系统下的七种文件类型之一,其作用类似于 Windows 下的快捷方式。那么硬链接文件又是什么呢?本小节就来聊一聊它们之间的区别。
首先,从使用角度来讲,两者没有任何区别,都与正常的文件访问方式一样,支持读、写以及执行。那它们的区别在哪呢?在底层原理上, 为了说明这个问题, 先来创建一个硬链接文件,如下所示:

Tips:使用 ln 命令可以为一个文件创建软链接文件或硬链接文件,用法如下:
硬链接: ln 源文件 链接文件
软链接: ln -s 源文件 链接文件
关于该命令其它用法,可以查看 man 手册。
从图中可知,使用 ln 命令创建的两个硬链接文件与源文件 test_file 都拥有相同的 inode 号, 既然inode 相同,也就意味着它们指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已,创建出来的硬链接文件与源文件对文件系统来说是完全平等的关系。那么大家可能要问了,如果删除了硬链接文件或源文件其中之一,那文件所对应的 inode 以及文件内容在磁盘中的数据块会被文件系统回收吗? 事实上并不会这样,因为 inode 数据结结构中会记录文件的链接数,这个链接数指的就是硬链接数, struct stat 结构体中的st_nlink 成员变量就记录了文件的链接数,这些内容前面已经给大家介绍过了。当为文件每创建一个硬链接, inode 节点上的链接数就会加一,每删除一个硬链接, inode 节点上的链接
数就会减一,直到为 0, inode 节点和对应的数据块才会被文件系统所回收,也就意味着文件已经从文件系统中被删除了。使用"ls -li"命令查看到, 此时链接数为 3(dt 用户名前面的那个数字),我们明明创建了 2 个链接文件,为什么链接数会是 3?其实源文件 test_file 本身就是一个硬链接文件,所以这里才是 3。
当我们删除其中任何一个文件后,链接数就会减少,如下所示:

接下来再来聊一聊软链接文件,软链接文件与源文件有着不同的 inode 号, 如图所示, 所以也就是意味着它们之间有着不同的数据块,但是软链接文件的数据块中存储的是源文件的路径名,链接文件可以通过这个路径找到被链接的源文件,它们之间类似于一种“主从”关系,当源文件被删除之后,软链接文件依然存在,但此时它指向的是一个无效的文件路径, 这种链接文件被称为悬空链接, 如图所示。

从图中还可看出, inode 节点中记录的链接数并未将软链接计算在内。
介绍完它们之间的区别之后, 大家可能觉得硬链接相对于软链接来说有较大的优势,其实并不是这样,对于硬链接来说, 存在一些限制情况,如下:
⚫ 不能对目录创建硬链接(超级用户可以创建,但必须在底层文件系统支持的情况下)。
⚫ 硬链接通常要求链接文件和源文件位于同一文件系统中。

而软链接文件的使用并没有上述限制条件,优点如下所示:
⚫ 可以对目录创建软链接;
⚫ 可以跨越不同文件系统;
⚫ 可以对不存在的文件创建软链接。

创建链接文件

在 Linux 系统下,可以使用系统调用创建硬链接文件或软链接文件,本小节向大家介绍如何通过这些系统调用创建链接文件。

创建硬链接 link()

link()系统调用用于创建硬链接文件,函数原型如下(可通过"man 2 link"命令查看):

#include <unistd.h>
int link(const char *oldpath, const char *newpath);

首先,使用该函数需要包含头文件<unistd.h>。
函数原型和返回值含义如下:
oldpath: 用于指定被链接的源文件路径,应避免 oldpath 参数指定为软链接文件,为软链接文件创建硬链接没有意义,虽然并不会报错。
newpath: 用于指定硬链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误。
返回值: 成功返回 0;失败将返回-1,并且会设置 errno。

接下来我们编写一个简单地程序,演示 link 函数如何使用:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    int ret;
    ret = link("./test_file", "./hard");
    if (-1 == ret) {
        perror("link error");
        exit(-1);
    }

    exit(0);
}

程序中通过 link 函数为当前目录下的 test_file 文件创建了一个硬链接 hard,编译测试:

创建软链接 symlink()

symlink()系统调用用于创建软链接文件,函数原型如下(可通过"man 2 symlink"命令查看):

#include <unistd.h>
int symlink(const char *target, const char *linkpath);

首先,使用该函数需要包含头文件<unistd.h>。
函数参数和返回值含义如下:
target: 用于指定被链接的源文件路径, target 参数指定的也可以是一个软链接文件。
linkpath: 用于指定软链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误。
返回值: 成功返回 0;失败将返回-1,并会设置 errno。
创建软链接时,并不要求 target 参数指定的文件路径已经存在,如果文件不存在,那么创建的软链接将成为“悬空链接”。
 

接下来我们编写一个简单地程序,演示 symlink 函数如何使用:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    int ret;
    ret = symlink("./test_file", "./soft");
    if (-1 == ret) {
      
上一篇:C++ list 介绍-????三、list的特殊功能函数


下一篇:cpp笔记-24-05-10