文件属性与目录

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

Linux 系统中的文件类型(7种)

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 命令来说,并没有直观的显示出文件的类型,而是通过符号表示出来,在图5.1.2 中画红色框位置显示出的一串字符中,其中第一个字符(’ - ‘)就用于表示文件的类型,减号’ - '就表示该文件是一个普通文件;除此之外,来看看其它文件类型使用什么字符表示:

⚫ ’ - ':普通文件
⚫ ’ 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 下的符号链接文件。图5.1.4 中的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);

函数参数及返回值含义如下:
pathname:用于指定一个需要查看属性的文件路径。
buf:struct stat 类型指针,用于指向一个struct stat 结构体变量。调用stat 函数的时候需要传入一个struct stat 变量的指针,获取到的文件属性信息就记录在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:该字段用于描述文件的模式,譬如文件类型、文件权限都记录在该变量中,关于该变量的介绍请看5.2.2 小节。
st_nlink:该字段用于记录文件的硬链接数,也就是为该文件创建了多少个硬链接文件。链接文件可以分为软链接(符号链接)文件和硬链接文件,关于这些内容后面再给大家介绍。
st_uid、st_gid:此两个字段分别用于描述文件所有者的用户ID 以及文件所有者的组ID,后面再给大家介绍。
st_rdev:该字段记录了设备号,设备号只针对于设备文件,包括字符设备文件和块设备文件,不用理会。
st_size:该字段记录了文件的大小(逻辑大小),以字节为单位。
st_atim、st_mtim、st_ctim:此三个字段分别用于记录文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后被改变的时间,都是struct timespec 类型变量,具体介绍请看5.2.3 小节。

st_mode 变量

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

在这里插入图片描述

看到图5.2.1 的时候,大家有没有似曾相识的感觉,确实,前面章节内容给大家介绍open 函数的第三个参数mode 时也用到了类似的图,如图2.3.2 所示。唯一不同的在于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))
{
    /* 是*/
}

关于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
下被称为日历时间,7.2 小计中对此有详细介绍。
由示例代码5.2.1 可知,struct stat 结构体中包含了三个文件相关的时间属性,但这里得到的仅仅只是以秒+微秒为单位的时间值,对于我们来说,并不利用查看,我们一般喜欢的是“2020-10-10 18:30:30”这种形式表示的时间,直观、明了,那有没有办法通过秒来得到这种形式表达的时间呢?答案当然是可以,譬如可以通过localtime()/localtime_r()或者strftime()来得到更利于我们查看的时间表达方式,关于这些函数的介绍以及使用方法在7.2.4 小节有详细说明。

练习

到这里本小节内容就给大家介绍完了,主要给大家介绍了stat 函数以及由此引出来的一系列知识内容。为了巩固本小节所学内容,这里出一些简单地编程练习题,大家可以根据本小节所学知识完成它。
(1)获取文件的inode 节点编号以及文件大小,并将它们打印出来。
(2)获取文件的类型,判断此文件对于其它用户(Other)是否具有可读可写权限。
(3)获取文件的时间属性,包括文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后被改变的时间,并使用字符串形式将其打印出来,包括时间和日期、表示形式自定。
以上就是根据本小节内容整理出来的一些简单的编程练习题,下面笔者将给出对应的示例代码。
(1)编程实战练习1

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    struct stat file_stat;
    int ret;
    /* 获取文件属性*/
    ret = stat("./test_file", &file_stat);
    if (-1 == ret)
    {
        perror("stat error");
        exit(-1);
    }
    /* 打印文件大小和inode 编号*/
    printf("file size: %ld bytes\n"
           "inode number: %ld\n",
           file_stat.st_size,
           file_stat.st_ino);
    exit(0);
}

测试之前先使用ls 命令查看test_file 文件的inode 节点和大小,如下:
在这里插入图片描述
从图中可以得知,此文件的大小为8864 个字节,inode 编号为3701841;接下来编译我们的测试程序、并运行:
在这里插入图片描述
(2)编程实战练习2

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    struct stat file_stat;
    int ret;
    /* 获取文件属性*/
    ret = stat("./test_file", &file_stat);
    if (-1 == ret)
    {
        perror("stat error");
        exit(-1);
    }
    /* 判读文件类型*/
    switch (file_stat.st_mode & S_IFMT)
    {
    case S_IFSOCK:
        printf("socket");
        break;
    case S_IFLNK:
        printf("symbolic link");
        break;
    case S_IFREG:
        printf("regular file");
        break;
    case S_IFBLK:
        printf("block device");
        break;
    case S_IFDIR:
        printf("directory");
        break;
    case S_IFCHR:
        printf("character device");
        break;
    case S_IFIFO:
        printf("FIFO");
        break;
    }
    printf("\n");
    /* 判断该文件对其它用户是否具有读权限*/
    if (file_stat.st_mode & S_IROTH)
        printf("Read: Yes\n");
    else
        printf("Read: No\n");
    /* 判断该文件对其它用户是否具有写权限*/
    if (file_stat.st_mode & S_IWOTH)
        printf("Write: Yes\n");
    else
        printf("Write: No\n");
    exit(0);
}

测试:
在这里插入图片描述
(3)编程实战练习3

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    struct stat file_stat;
    struct tm file_tm;
    char time_str[100];
    int ret;
    /* 获取文件属性*/
    ret = stat("./test_file", &file_stat);
    if (-1 == ret)
    {
        perror("stat error");
        exit(-1);
    }
    /* 打印文件最后被访问的时间*/
    localtime_r(&file_stat.st_atim.tv_sec, &file_tm);
    strftime(time_str, sizeof(time_str),
             "%Y-%m-%d %H:%M:%S", &file_tm);
    printf("time of last access: %s\n", time_str);
    /* 打印文件内容最后被修改的时间*/
    localtime_r(&file_stat.st_mtim.tv_sec, &file_tm);
    strftime(time_str, sizeof(time_str),
             "%Y-%m-%d %H:%M:%S", &file_tm);
    printf("time of last modification: %s\n", time_str);
    /* 打印文件状态最后改变的时间*/
    localtime_r(&file_stat.st_ctim.tv_sec, &file_tm);
    strftime(time_str, sizeof(time_str),
             "%Y-%m-%d %H:%M:%S", &file_tm);
    printf("time of last status change: %s\n", time_str);
    exit(0);
}

测试:
在这里插入图片描述
可以使用stat 命令查看test_file 文件的这些时间属性,对比程序打印出来是否正确:
在这里插入图片描述

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)就可以识别出不同的用户和用户组。
Tips:用户ID 简称UID、用户组ID 简称GID。这些都是Linux 操作系统的基础知识,如果对用户和用户组的概念尚不熟悉,建议先自行学习这些基础知识。
譬如使用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 用于文件访问权限检查,详情请查看5.4.1 小节内容。

有效用户ID 和有效组ID

首先对于有效用户ID 和有效组ID 来说,这是进程所持有的概念,对于文件来说,并无此属性!有效用户ID 和有效组ID 是站在操作系统的角度,用于给操作系统判断当前执行该进程的用户在当前环境下对某个文件是否拥有相应的权限。
在Linux 系统中,当进程对文件进行读写操作时,系统首先会判断该进程是否具有对该文件的读写权限,那如何判断呢?自然是通过该文件的权限位来判断,struct stat 结构体中的st_mode 字段中就记录了该文件的权限位以及文件类型。关于文件权限检查相关内容将会在5.5 小节中说明。
当进行权限检查时,并不是通过进程的实际用户和实际组来参与权限检查的,而是通过有效用户和有效组来参与文件权限检查。通常,绝大部分情况下,进程的有效用户等于实际用户(有效用户ID 等于实际用户ID),有效组等于实际组(有效组ID 等于实际组ID)。
那么大家可能就要问了,什么情况下有效用户ID 不等于实际用户ID、有效组ID 不等于实际组ID?那么关于这个问题,后面将给大家揭晓!
Tips:文中所指的"进程对文件是否拥有xx 权限"其实质是当前执行该进程的用户是否拥有对文件的xx
权限。若无特别指出,文中的描述均为此意!

chown 函数

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

sudo chown root:root testApp.c

图5.4.2 使用chown 命令修改文件所有者和所属组

可以看到,通过该命令确实可以改变文件的所有者和所属组,这个命令内部其实就是调用了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,那么也就意味着任何普通用户对系统文件都有任意权限了,这对于操作系统来说将是非常不安全的。
测试
接下来看一些chown 函数的使用例程,如下所示:

#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);

我们可以在示例代码5.4.1 中加入打印用户ID 的语句,如下所示:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    printf("uid: %d\n", getuid());
    if (-1 == chown("./test_file", 0, 0))
    {
        perror("chown error");
        exit(-1);
    }
    exit(0);
}

再来重复上面的测试:
在这里插入图片描述
很明显可以看到两次执行同一个应用程序它们的用户ID 是不一样的,因为加上了sudo 使得应用程序的用户ID 由原本的普通用户ID 1000 变成了超级用户ID 0,使得该进程变成了超级用户进程,所以调用
chown 函数就不会报错。
关于chown 就给大家介绍这么多,在实际应用编程中,此系统调用被用到的概率并不多,但是理论性知识还是得知道。

fchown 和lchown 函数

这两个同样也是系统调用,作用与chown 函数相同,只是参数、细节方面有些许不同。fchown()、lchown()
这两个函数与chown()的区别就像是fstat()、lstat()与stat 的区别,本小节就不再重述这种问题了,如果大家对此还不清楚,可以看5.3 小节,具体使用fchown、lchown 还是chown,看情况而定。

文件访问权限

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

普通权限和特殊权限

文件的权限可以分为两个大类,分别是普通权限和特殊权限(也可称为附加权限)。普通权限包括对文件的读、写以及执行,而特殊权限则包括一些对文件的附加权限,譬如Set-User-ID、Set-Group-ID 以及Sticky。接下来,分别对普通权限和特殊权限进行介绍。
普通权限
每个文件都有9 个普通的访问权限位,可将它们分为3 类,如下表:

在这里插入图片描述

譬如使用ls 命令或stat 命令可以查看到文件的这9 个访问权限,如下所示:

在这里插入图片描述

每一行打印信息中,前面的一串字符串就描述了该文件的9 个访问权限以及文件类型,譬如"-rwxrwxr-
x":

在这里插入图片描述

最前面的一个字符表示该文件的类型,这个前面给大家介绍过," - "表示该文件是一个普通文件。
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 个特殊权限,也就是图5.2.1 中所表示的S 字段权限位,S 字段三个bit 位中,从高位到低位依次表示文件的set-user-ID 位权限、set-group-ID 位权限以及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)。

目录权限

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

图5.5.3 创建文件、删除文件

那说明删除文件、创建文件这些操作也是需要相应权限的,那这些权限又是从哪里获取的呢?答案就是目录。目录(文件夹)在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。
测试
通过access 函数检查文件是否存在,若存在、则继续检查执行进程的用户对该文件是否有读、写、执行权限。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define MY_FILE "./test_file"
int main(void)
{
    int ret;
    /* 检查文件是否存在*/
    ret = access(MY_FILE, F_OK);
    if (-1 == ret)
    {
        printf("%: file does not exist.\n", MY_FILE);
        exit(-1);
    }
    /* 检查权限*/
    ret = access(MY_FILE, R_OK);
    if (!ret)
        printf("Read permission: Yes\n");
    else
        printf("Read permission: NO\n");
    ret = access(MY_FILE, W_OK);
    if (!ret)
        printf("Write permission: Yes\n");
    else
        printf("Write permission: NO\n");
    ret = access(MY_FILE, X_OK);
    if (!ret)
        printf("Execution permission: Yes\n");
    else
        printf("Execution permission: NO\n");
    exit(0);
}

接下来编译测试:
在这里插入图片描述

修改文件权限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,其它功能都是一样的。
测试

#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int ret;
    ret = chmod("./test_file", 0777);
    if (-1 == ret)
    {
        perror("chmod error");
        exit(-1);
    }
    exit(0);
}

上述代码中,通过调用chmod 函数将当前目录下的test_file 文件,其权限修改为0777(八进制表示方式,也可以使用S_IRUSR、S_IWUSR 等这些宏来表示),也就是文件所有者、文件所属组用户以及其它用户都拥有读、写、执行权限,接下来编译测试:
在这里插入图片描述
执行程序之前,test_file 文件的权限为rw-r–r–(0644),程序执行完成之后,再次查看文件权限为
rwxrwxrwx(0777),修改成功!

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:需要设置的权限掩码值,可以发现make 参数的类型与open 函数、chmod 函数中的mode 参数对应的类型一样,所以其表示方式也是一样的,前面也给大家介绍了,既可以使用数字表示(譬如八进制数)也可以直接使用宏(S_IRUSR、S_IWUSR 等)。
返回值:返回设置之前的umask 值,也就是旧的umask。
测试
接下来我们编写一个测试代码,使用umask()函数修改进程的umask 权限掩码,测试代码如下所示:

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    mode_t old_mask;
    old_mask = umask(0003);
    printf("old mask: %04o\n", old_mask);
    exit(0);
}

上述代码中,使用umask 函数将该进程的umask 设置为0003(八进制),返回得到的old_mask 则是设置之前旧的umask 值,然后将其打印出来:
在这里插入图片描述
从打印信息可以看出,旧的umask 等于0002,这个umask 是从当前vscode 的shell 终端继承下来的,如果没有修改进程的umask 值,默认就是从父进程继承下来的umask。
这里再次强调,umask 是进程自身的一种属性、A 进程的umask 与B 进程的umask 无关(父子进程关系除外)。在shell 终端下可以使用umask 命令设置shell 终端的umask 值,但是该shell 终端关闭之后、再次打开一个终端,新打开的终端将与之前关闭的终端并无任何瓜葛!

文件的时间属性

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

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

文件的时间属性虽然会在我们对文件进行相关操作(譬如:读、写)的时候发生改变,但这些改变都是隐式、被动的发生改变,除此之外,还可以使用Linux 系统提供的系统调用显式的修改文件的时间属性。本小节给大家介绍如何使用utime()和utimes()函数来修改文件的时间属性。
Tips:只能显式修改文件的最后一次访问时间和文件内容最后被修改的时间,不能显式修改文件状态最后被改变的时间,大家可以想一想为什么?笔者把这个作为思考题留给大家!
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 测试
接下来我们编写一个简单地测试程序,使用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()相同的时间戳修改权限规则。
utimes 测试

#include <unistd.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#define MY_FILE "./test_file"
int main(void)
{
    struct timeval tmval_arr[2];
    time_t cur_sec;
    int ret;
    int i;
    /* 检查文件是否存在*/
    ret = access(MY_FILE, F_OK);
    if (-1 == ret)
    {
        printf("Error: %s file does not exist!\n", MY_FILE);
        exit(-1);
    }
    /* 获取当前时间*/
    time(&cur_sec);
    for (i = 0; i < 2; i++)
    {
        tmval_arr[i].tv_sec = cur_sec;
        tmval_arr[i].tv_usec = 0;
    }
    /* 修改文件时间戳*/
    ret = utimes(MY_FILE, tmval_arr);
    if (-1 == ret)
    {
        perror("utimes error");
        exit(-1);
    }
    exit(0);
}

代码不再给大家进行介绍了,功能与示例代码5.6.2 相同,大家可以自己动手编译、运行测试。

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 结构体类型变量的数组,数组共有两个元素,第一个元素用于指定访问时间,第二个元素用于指定内容修改时间,该结构体在5.2.3 小节给大家介绍过了,这里不再重述!
返回值:成功返回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
字段指定的值。
Tips:UTIME_NOW 和UTIME_OMIT 是两个宏定义。
使用futimens()函数只有以下进程,可对文件时间戳进行修改:
⚫ 超级用户进程。
⚫ 在参数times 等于NULL 的情况下,对文件拥有写权限的进程。
⚫ 有效用户ID 与该文件用户ID(文件所有者)相匹配的进程。

futimens()测试

#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,则实际操作的文件路径是相对于调用进程的当前工作目录进行解析,关于进程的工作目录在5.7 小节中有介绍。
times:与futimens()的times 参数含义相同。
flags :此参数可以为0 ,也可以设置为AT_SYMLINK_NOFOLLOW ,如果设置为
AT_SYMLINK_NOFOLLOW,当pathname 参数指定的文件是符号链接,则修改的是该符号链接的时间戳,而不是它所指向的文件。
返回值:成功返回0;失败返回-1、并会设置时间戳。
utimensat()遵循与futimens()相同的时间戳修改权限规则。
utimensat()函数测试

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define MY_FILE "/home/dt/vscode_ws/2_chapter/test_file"
int main(void)
{
    struct timespec tmsp_arr[2];
    int ret;
    /* 检查文件是否存在*/
    ret = access(MY_FILE, F_OK);
    if (-1 == ret)
    {
        printf("Error: %s file does not exist!\n", MY_FILE);
        exit(-1);
    }
/* 修改文件时间戳*/
#if 1
    ret = utimensat(-1, MY_FILE, NULL, AT_SYMLINK_NOFOLLOW); // 同时设置为当前时间
#endif
#if 0
tmsp_arr[0].tv_nsec = UTIME_OMIT;//访问时间保持不变
tmsp_arr[1].tv_nsec = UTIME_NOW;//内容修改时间设置为当期时间
ret = utimensat(-1, MY_FILE, tmsp_arr, AT_SYMLINK_NOFOLLOW);
#endif
#if 0
tmsp_arr[0].tv_nsec = UTIME_NOW;//访问时间设置为当前时间
tmsp_arr[1].tv_nsec = UTIME_OMIT;//内容修改时间保持不变
ret = utimensat(-1, MY_FILE, tmsp_arr, AT_SYMLINK_NOFOLLOW);
#endif
    if (-1 == ret)
    {
        perror("futimens error");
        exit(-1);
    }
    exit(0);
}

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

在Linux 系统中有两种链接文件,分为软链接(也叫符号链接)文件和硬链接文件,软链接文件也就是前面给大家的Linux 系统下的七种文件类型之一,其作用类似于Windows 下的快捷方式。那么硬链接文件又是什么呢?本小节就来聊一聊它们之间的区别。
首先,从使用角度来讲,两者没有任何区别,都与正常的文件访问方式一样,支持读、写以及执行。那它们的区别在哪呢?在底层原理上,为了说明这个问题,先来创建一个硬链接文件,如下所示:
在这里插入图片描述
Tips:使用ln 命令可以为一个文件创建软链接文件或硬链接文件,用法如下:
硬链接:ln 源文件链接文件
软链接:ln -s 源文件链接文件
关于该命令其它用法,可以查看man 手册。
从图5.7.1 中可知,使用ln 命令创建的两个硬链接文件与源文件test_file 都拥有相同的inode 号,既然
inode 相同,也就意味着它们指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已,创建出来的硬链接文件与源文件对文件系统来说是完全平等的关系。那么大家可能要问了,如果删除了硬链接文件或源文件其中之一,那文件所对应的inode 以及文件内容在磁盘中的数据块会被文件系统回收吗?事实上并不会这样,因为inode 数据结结构中会记录文件的链接数,这个链接数指的就是硬链接数,struct stat 结构体中的
st_nlink 成员变量就记录了文件的链接数,这些内容前面已经给大家介绍过了。
当为文件每创建一个硬链接,inode 节点上的链接数就会加一,每删除一个硬链接,inode 节点上的链接数就会减一,直到为0,inode 节点和对应的数据块才会被文件系统所回收,也就意味着文件已经从文件系统中被删除了。从图5.7.1 中可知,使用"ls -li"命令查看到,此时链接数为3(dt 用户名前面的那个数字),我们明明创建了2 个链接文件,为什么链接数会是3?其实源文件test_file 本身就是一个硬链接文件,所以这里才是3。
当我们删除其中任何一个文件后,链接数就会减少,如下所示:
在这里插入图片描述
接下来再来聊一聊软链接文件,软链接文件与源文件有着不同的inode 号,如图5.7.3 所示,所以也就是意味着它们之间有着不同的数据块,但是软链接文件的数据块中存储的是源文件的路径名,链接文件可以通过这个路径找到被链接的源文件,它们之间类似于一种“主从”关系,当源文件被删除之后,软链接文件依然存在,但此时它指向的是一个无效的文件路径,这种链接文件被称为悬空链接,如图5.7.4 所示。
在这里插入图片描述
在这里插入图片描述
从图中还可看出,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 函数测试
接下来我们编写一个简单地程序,演示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 函数测试
接下来我们编写一个简单地程序,演示symlink 函数如何使用:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    int ret;
    ret = symlink("./test_file", "./soft");
    if (-1 == ret)
    {
        perror("symlink error");
        exit(-1);
    }
    exit(0);
}

程序中通过symlink 函数为当前目录下的test_file 文件创建了一个软链接soft,编译测试:
在这里插入图片描述

读取软链接文件

前面给大家介绍到,软链接文件数据块中存储的是被链接文件的路径信息,那如何读取出软链接文件中存储的路径信息呢?大家认为使用read 函数可以吗?答案是不可以,因为使用read 函数之前,需要先open
打开该文件得到文件描述符,但是调用open 打开一个链接文件本身是不会成功的,因为打开的并不是链接文件本身、而是其指向的文件,所以不能使用read 来读取,那怎么办呢?可以使用系统调用readlink。
readlink 函数原型如下所示:

#include <unistd.h>
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);

函数参数和返回值含义如下:
pathname:需要读取的软链接文件路径。只能是软链接文件路径,不能是其它类型文件,否则调用函数将报错。
buf:用于存放路径信息的缓冲区。
bufsiz:读取大小,一般读取的大小需要大于链接文件数据块中存储的文件路径信息字节大小。
返回值:失败将返回-1,并会设置errno;成功将返回读取到的字节数。
readlink 函数测试
接下来我们编写一个简单地程序,演示readlink 函数如何使用:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
    char buf[50];
    int ret;
    memset(buf, 0x0, sizeof(buf));
    ret = readlink("./soft", buf, sizeof(buf));
    if (-1 == ret)
    {
        perror("readlink error");
        exit(-1);
    }
    printf("%s\n", buf);
    exit(0);
}

使用readlink 函数读取当前目录下的软链接文件soft,并将读取到的信息打印出来,测试如下:
在这里插入图片描述

目录

目录(文件夹)在Linux 系统也是一种文件,是一种特殊文件,同样可以使用前面给大家介绍open、
read 等这些系统调用以及C 库函数对其进行操作,但是目录作为一种特殊文件,并不适合使用前面介绍的文件I/O 方式进行读写等操作。在Linux 系统下,会有一些专门的系统调用或C 库函数用于对文件夹进行操作,譬如:打开、创建文件夹、删除文件夹、读取文件夹以及遍历文件夹中的文件等,那么本小节将向大家介绍目录相关的知识内容。

目录存储形式

3.1 小节中给大家介绍了普通文件的管理形式或存储形式,本小节聊一聊目录这种特殊文件在文件系统中的存储形式,其实目录在文件系统中的存储方式与常规文件类似,常规文件包括了inode 节点以及文件内容数据存储块(block),参考图3.1.1 所示;但对于目录来说,其存储形式则是由inode 节点和目录块所构成,目录块当中记录了有哪些文件组织在这个目录下,记录它们的文件名以及对应的inode 编号。
其存储形式如下图所示:
在这里插入图片描述
目录块当中有多个目录项(或叫目录条目),每一个目录项(或目录条目)都会对应到该目录下的某一个文件,目录项当中记录了该文件的文件名以及它的inode 节点编号,所以通过目录的目录块便可以遍历找到该目录下的所有文件以及所对应的inode 节点。
所以对此总结如下:
⚫ 普通文件由inode 节点和数据块构成
⚫ 目录由inode 节点和目录块构成

创建和删除目录

使用open 函数可以创建一个普通文件,但不能用于创建目录文件,在Linux 系统下,提供了专门用于创建目录mkdir()以及删除目录rmdir 相关的系统调用。

mkdir 函数
函数原型如下所示:

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

函数参数和返回值含义如下:
pathname:需要创建的目录路径。
mode:新建目录的权限设置,设置方式与open 函数的mode 参数一样,最终权限为(mode & ~umask)。
返回值:成功返回0;失败将返回-1,并会设置errno。
pathname 参数指定的新建目录的路径,该路径名可以是相对路径,也可以是绝对路径,若指定的路径名已经存在,则调用mkdir()将会失败。
mode 参数指定了新目录的权限,目录拥有与普通文件相同的权限位,但是其表示的含义与普通文件却有不同,5.5.2 小计对此作了说明。
mkdir 函数测试

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(void)
{
    int ret;
    ret = mkdir("./new_dir", S_IRWXU |
                                 S_IRGRP | S_IXGRP |
                                 S_IROTH | S_IXOTH);
    if (-1 == ret)
    {
        perror("mkdir error");
        exit(-1);
    }
    exit(0);
}

上述代码中,我们通过mkdir 函数在当前目录下创建了一个目录new_dir,并将其权限设置为0755(八进制),编译运行:
在这里插入图片描述
rmdir 函数
rmdir()用于删除一个目录

#include <unistd.h>
int rmdir(const char *pathname);

首先,使用该函数需要包含头文件<unistd.h>。
函数参数和返回值含义如下:
pathname:需要删除的目录对应的路径名,并且该目录必须是一个空目录,也就是该目录下只有.和…这两个目录项;pathname 指定的路径名不能是软链接文件,即使该链接文件指向了一个空目录。
返回值:成功返回0;失败将返回-1,并会设置errno。
rmdir 函数测试

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    int ret;
    ret = rmdir("./new_dir");
    if (-1 == ret)
    {
        perror("rmdir error");
        exit(-1);
    }
    exit(0);
}

打开、读取以及关闭目录

打开、读取、关闭一个普通文件可以使用open()、read()、close(),而对于目录来说,可以使用opendir()、
readdir()和closedir()来打开、读取以及关闭目录,接下来将向大家介绍这3 个C 库函数的用法。
打开文件opendir
opendir()函数用于打开一个目录,并返回指向该目录的句柄,供后续操作使用。Opendir 是一个C 库函数,opendir()函数原型如下所示:

#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);

函数参数和返回值含义如下:
name:指定需要打开的目录路径名,可以是绝对路径,也可以是相对路径。
返回值:成功将返回指向该目录的句柄,一个DIR 指针(其实质是一个结构体指针),其作用类似于
open 函数返回的文件描述符fd,后续对该目录的操作需要使用该DIR 指针变量;若调用失败,则返回NULL。
读取目录readdir
readdir()用于读取目录,获取目录下所有文件的名称以及对应inode 号。这里给大家介绍的readdir()是一个C 库函数(事实上Linux 系统还提供了一个readdir 系统调用),其函数原型如下所示:

#include <dirent.h>
struct dirent *readdir(DIR *dirp);

首先,使用该函数需要包含头文件<dirent.h>。
函数参数和返回值含义如下:
dirp:目录句柄DIR 指针。
返回值:返回一个指向struct dirent 结构体的指针,该结构体表示dirp 指向的目录流中的下一个目录条目。在到达目录流的末尾或发生错误时,它返回NULL。
Tips:“流”是从自然界中抽象出来的一种概念,有点类似于自然界当中的水流,在文件操作中,文件内容数据类似池塘中存储的水,N 个字节数据被读取出来或将N 个字节数据写入到文件中,这些数据就构成了字节流。
“流”这个概念是动态的,而不是静态的。编程当中提到这个概念,一般都是与I/O 相关,所以也经常叫做I/O 流;但对于目录这种特殊文件来说,这里将目录块中存储的数据称为目录流,存储了一个一个的目录项(目录条目)。
struct dirent 结构体内容如下所示:

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 编号和文件名,其余字段并不是所有系统都支持,所以也不再给大家介绍,这些字段一般也不会使用到。
每调用一次readdir(),就会从drip 所指向的目录流中读取下一条目录项(目录条目),并返回struct dirent
结构体指针,指向经静态分配而得的struct dirent 类型结构,每次调用readdir()都会覆盖该结构。一旦遇到目录结尾或是出错,readdir()将返回NULL,针对后一种情况,还会设置errno 以示具体错误。那如何区别究竟是到了目录末尾还是出错了呢,可通过如下代码进行判断:

error = 0;
direntp = readdir(dirp);
if (NULL == direntp)
{
    if (0 != error)
    {
        /* 出现了错误*/
    }
    else
    {
        /* 已经到了目录末尾*/
    }
}

使用readdir()返回时并未对文件名进行排序,而是按照文件在目录中出现的天然次序(这取决于文件系统向目录添加文件时所遵循的次序,及其在删除文件后对目录列表中空隙的填补方式)。
当使用opendir()打开目录时,目录流将指向了目录列表的头部(0),使用readdir()读取一条目录条目之后,目录流将会向后移动、指向下一个目录条目。这其实跟open()类似,当使用open()打开文件的时候,文件位置偏移量默认指向了文件头部,当使用read()或write()进行读写时,文件偏移量会自动向后移动。
rewinddir 函数
rewinddir()是C 库函数,可将目录流重置为目录起点,以便对readdir()的下一次调用将从目录列表中的第一个文件开始。rewinddir 函数原型如下所示:

#include <sys/types.h>
#include <dirent.h>
void rewinddir(DIR *dirp);

首先,使用该函数需要包含头文件<dirent.h>。
函数参数和返回值含义如下:
dirp:目录句柄。
返回值:无返回值。
关闭目录closedir 函数
closedir()函数用于关闭处于打开状态的目录,同时释放它所使用的资源,其函数原型如下所示:

#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);

首先,使用该函数需要包含头文件<sys/types.h>和<dirent.h>。
函数参数和返回值含义如下:
dirp:目录句柄。
返回值:成功返回0;失败将返回-1,并设置errno。
练习
根据本小节所学知识内容,可以做一个简单地编程练习,打开一个目录、并将目录下的所有文件的名称以及其对应inode 编号打印出来。示例代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <errno.h>
int main(void)
{
    struct dirent *dir;
    DIR *dirp;
    int ret = 0;
    /* 打开目录*/
    dirp = opendir("./my_dir");
    if (NULL == dirp)
    {
        perror("opendir error");
        exit(-1);
    }
    /* 循环读取目录流中的所有目录条目*/
    errno = 0;
    while (NULL != (dir = readdir(dirp)))
        printf("%s %ld\n", dir->d_name, dir->d_ino);
    if (0 != errno)
    {
        perror("readdir error");
        ret = -1;
        goto err;
    }
    else
        printf("End of directory!\n");
err:
    closedir(dirp);
    exit(ret);
}

使用opendir()打开了当前目录下的my_dir 目录,该目录下的文件如下所示:
在这里插入图片描述
接下来编译、运行:
在这里插入图片描述
由此可知,示例代码5.8.4 能够将my_dir 目录下的所有文件全部扫描出来,打印出它们的名字以及inode
节点。

进程的当前工作目录

Linux 下的每一个进程都有自己的当前工作目录(current working directory),当前工作目录是该进程解析、搜索相对路径名的起点(不是以" / "斜杆开头的绝对路径)。譬如,代码中调用open 函数打开文件时,传入的文件路径使用相对路径方式进行表示,那么该进程解析这个相对路径名时、会以进程的当前工作目录作为参考目录。
一般情况下,运行一个进程时、其父进程的当前工作目录将被该进程所继承,成为该进程的当前工作目录。可通过getcwd 函数来获取进程的当前工作目录,如下所示:

#include <unistd.h>
char *getcwd(char *buf, size_t size);

这是一个系统调用,使用该函数之前,需要包含头文件<unistd.h>。
函数参数和返回值含义如下:
buf:getcwd()将内含当前工作目录绝对路径的字符串存放在buf 缓冲区中。
size:缓冲区的大小,分配的缓冲区大小必须要大于字符串长度,否则调用将会失败。
返回值:如果调用成功将返回指向buf 的指针,失败将返回NULL,并设置errno。
Tips:若传入的buf 为NULL,且size 为0,则getcwd()内部会按需分配一个缓冲区,并将指向该缓冲区的指针作为函数的返回值,为了避免内存泄漏,调用者使用完之后必须调用free()来释放这一缓冲区所占内存空间。
测试
接下来,我们编写一个简单地测试程序用于读取进程的当前工作目录:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
    char buf[100];
    char *ptr;
    memset(buf, 0x0, sizeof(buf));
    ptr = getcwd(buf, sizeof(buf));
    if (NULL == ptr)
    {
        perror("getcwd error");
        exit(-1);
    }
    printf("Current working directory: %s\n", buf);
    exit(0);
}

编译运行:
在这里插入图片描述
改变当前工作目录
系统调用chdir()和fchdir()可以用于更改进程的当前工作目录,函数原型如下所示:

#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);

首先,使用这两个函数之一需要包含头文件<unistd.h>。
函数参数和返回值含义如下:
path:将进程的当前工作目录更改为path 参数指定的目录,可以是绝对路径、也可以是相对路径,指定的目录必须要存在,否则会报错。
fd:将进程的当前工作目录更改为fd 文件描述符所指定的目录(譬如使用open 函数打开一个目录)。
返回值:成功均返回0;失败均返回-1,并设置errno。
此两函数的区别在于,指定目录的方式不同,chdir()是以路径的方式进行指定,而fchdir()则是通过文件描述符,文件描述符可调用open()打开相应的目录时获得。
测试

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
    char buf[100];
    char *ptr;
    int ret;
    /* 获取更改前的工作目录*/
    memset(buf, 0x0, sizeof(buf));
    ptr = getcwd(buf, sizeof(buf));
    if (NULL == ptr)
    {
        perror("getcwd error");
        exit(-1);
    }
    printf("Before the change: %s\n", buf);
    /* 更改进程的当前工作目录*/
    ret = chdir("./new_dir");
    if (-1 == ret)
    {
        perror("chdir error");
        exit(-1);
    }
    /* 获取更改后的工作目录*/
    memset(buf, 0x0, sizeof(buf));
    ptr = getcwd(buf, sizeof(buf));
    if (NULL == ptr)
    {
        perror("getcwd error");
        exit(-1);
    }
    printf("After the change: %s\n", buf);
    exit(0);
}

上述程序会在更改工作目录之前获取当前工作目录、并将其打印出来,之后调用chdir 函数将进程的工作目录更改为当前目录下的new_dir 目录,更改成功之后再将进程的当前工作目录获取并打印出来,接下来编译测试:
在这里插入图片描述

删除文件

前面给大家介绍了如何删除一个目录,使用rmdir()函数即可,显然该函数并不能删除一个普通文件,那如何删除一个普通文件呢?方法就是通过系统调用unlink()或使用C 库函数remove()。
使用unlink 函数删除文件
unlink()用于删除一个文件(不包括目录),函数原型如下所示:

#include <unistd.h>
int unlink(const char *pathname);

使用该函数需要包含头文件<unistd.h>。
函数参数和返回值含义如下:
pathname:需要删除的文件路径,可使用相对路径、也可使用绝对路径,如果pathname 参数指定的文件不存在,则调用unlink()失败。
返回值:成功返回0;失败将返回-1,并设置errno。
前面给大家介绍link 函数,用于创建一个硬链接文件,创建硬链接时,inode 节点上的链接数就会增加;
unlink()的作用与link()相反,unlink()系统调用用于移除/删除一个硬链接(从其父级目录下删除该目录条目)。
所以unlink()系统调用实质上是移除pathname 参数指定的文件路径对应的目录项(从其父级目录中移除该目录项),并将文件的inode 链接计数将1,如果该文件还有其它硬链接,则任可通过其它链接访问该文件的数据;只有当链接计数变为0 时,该文件的内容才可被删除。另一个条件也会阻止删除文件的内容-
–只要有进程打开了该文件,其内容也不能被删除。关闭一个文件时,内核会检查打开该文件的进程个数,如果这个计数达到0,内核再去检查其链接计数,如果链接计数也是0,那么就删除该文件对应的内容(也就是文件对应的inode 以及数据块被回收,如果一个文件存在多个硬链接,删除其中任何一个硬链接,其
inode 和数据块并没有被回收,还可通过其它硬链接访问文件的数据)。
unlink()系统调用并不会对软链接进行解引用操作,若pathname 指定的文件为软链接文件,则删除软链接文件本身,而非软链接所指定的文件。
测试

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    int ret;
    ret = unlink("./test_file");
    if (-1 == ret)
    {
        perror("unlink error");
        exit(-1);
    }
    exit(0);
}

上述代码调用unlink()删除当前目录下的test_file 文件,编译测试:
在这里插入图片描述
使用remove 函数删除文件
remove()是一个C 库函数,用于移除一个文件或空目录,其函数原型如下所示:

#include <stdio.h>
int remove(const char *pathname);

使用该函数需要包含C 库函数头文件<stdio.h>。
函数参数和返回值含义如下:
pathname:需要删除的文件或目录路径,可以是相对路径、也可是决定路径。
返回值:成功返回0;失败将返回-1,并设置errno。
pathname 参数指定的是一个非目录文件,那么remove()去调用unlink(),如果pathname 参数指定的是一个目录,那么remove()去调用rmdir()。
与unlink()、rmdir()一样,remove()不对软链接进行解引用操作,若pathname 参数指定的是一个软链接文件,则remove()会删除链接文件本身、而非所指向的文件。
测试

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int ret;
    ret = remove("./test_file");
    if (-1 == ret)
    {
        perror("remove error");
        exit(-1);
    }
    exit(0);
}

文件重命名

本小节给大家介绍rename()系统调用,借助于rename()既可以对文件进行重命名,又可以将文件移至同一文件系统中的另一个目录下,其函数原型如下所示:

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

使用该函数需要包含头文件<stdio.h>。
函数参数和返回值含义如下:
oldpath:原文件路径。
newpath:新文件路径。
返回值:成功返回0;失败将返回-1,并设置errno。
调用rename()会将现有的一个路径名oldpath 重命名为newpath 参数所指定的路径名。rename()调用仅操作目录条目,而不移动文件数据(不改变文件inode 编号、不移动文件数据块中存储的内容),重命名既不影响指向该文件的其它硬链接,也不影响已经打开该文件的进程(譬如,在重命名之前该文件已被其它进程打开了,而且还未被关闭)。
根据oldpath、newpath 的不同,有以下不同的情况需要进行说明:
⚫ 若newpath 参数指定的文件或目录已经存在,则将其覆盖;
⚫ 若newpath 和oldpath 指向同一个文件,则不发生变化(且调用成功)。
⚫ rename()系统调用对其两个参数中的软链接均不进行解引用。如果oldpath 是一个软链接,那么将重命名该软链接;如果newpath 是一个软链接,则会将其移除、被覆盖。
⚫ 如果oldpath 指代文件,而非目录,那么就不能将newpath 指定为一个目录的路径名。要想重命名一个文件到某一个目录下,newpath 必须包含新的文件名。
⚫ 如果oldpath 指代为一个目录,在这种情况下,newpath 要么不存在,要么必须指定为一个空目录。
⚫ oldpath 和newpath 所指代的文件必须位于同一文件系统。由前面的介绍,可以得出此结论!
⚫ 不能对.(当前目录)和…(上一级目录)进行重命名。
测试

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int ret;
    ret = rename("./test_file", "./new_file");
    if (-1 == ret)
    {
        perror("rename error");
        exit(-1);
    }
    exit(0);
}

将当前目录下的test_file 文件重命名为new_file,接下来编译测试:

在这里插入图片描述

从图中可以知道,使用rename 进行文件重命名之后,其inode 号并未改变。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行稳方能走远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值