目录
5.6.2 futimens()、utimensat()修改时间属性
本文参考正点原子教程,仅作为个人笔记使用。
在前面的章节内容中,都是围绕普通文件 I/O 操作进行的一系列讨论,譬如打开文件、读写文件、关闭文件等,本章将抛开文件 I/O 相关话题,来讨论 Linux 文件系统的其它特性以及文件相关属性;我们将从系统调用 stat 开始,可利用其返回一个包含多种文件属性(包括文件时间戳、文件所有权以及文件权限等)的结构体,逐个说明 stat 结构中的每一个成员以了解文件的所有属性,然后将向大家介绍用以改变文件属性的各种系统调用;除此之外,还会向大家介绍 Linux 系统中的符号链接以及目录相关的操作。
5.1 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
种文件类型,下面依次给大家介绍。
5.1.1 普通文件
普通文件(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 ' |
管道文件
|
5.1.2 目录文件
目录(directory
)就是文件夹,文件夹在
Linux
系统中也是一种文件,是一种特殊文件,同样我们也可以使用 vi
编辑器来打开文件夹,如下所示:

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

上图中 agpgart
、
autofs
、
btrfs-control
、
console
等这些都是字符设备文件,而
loop0
、
loop1
这些便是块设备文件。
5.1.4 符号链接文件
符号链接文件(link
)类似于
Windows
系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另一个文件路径,当对符号链接文件进行操作时,系统根据情况会对这个操作转移到它指向的文件上去,而不是对它本身进行操作,譬如,读取一个符号链接文件内容时,实际上读到的是它指向的文件的内容。
如果大家理解了 Windows 下的快捷方式,那么就会很容易理解
Linux
下的符号链接文件。上图
中的 cdrom
、
cdrw
、
fd
、
initctl
等这些文件都是符号链接文件,箭头所指向的文件路径便是符号链接文件所指向的文件。
关于链接文件,在后面的内容中还会给大家进行介绍,这里暂时给大家介绍这么多!
5.1.5 管道文件
管道文件(
pipe
)主要用于进程间通信,当学习到相关知识内容的时候再给大家详解。
5.1.6 套接字文件
套接字文件(socket
)也是一种进程间通信的方式,与管道文件不同的是,它们可以在不同主机上的进程间通信,实际上就是网络通信,当学习到网络编程相关知识内容再给大家介绍。
5.1.7 总结
下面对7 种文件类型进行一个简单地概括:
普通文件是最常见的文件类型;
目录也是一种文件类型;
设备文件对应于硬件设备;
符号链接文件类似于 Windows
的快捷方式;
管道文件用于进程间通信;
套接字文件用于网络通信。
5.2 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
函数的时候需要传入一个
struct stat 变量的指针,获取到的文件属性信息就记录在
struct stat
结构体中,稍后给大家介绍
struct stat
结构体中有记录了哪些信息。
返回值:成功返回
0
;失败返回
-1
,并设置
error
。
5.2.1 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
小节。
5.2.2 st_mode 变量
st_mode 是
structstat
结构体中的一个成员变量,是一个
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 permissionS_IRUSR 00400 owner has read permissionS_IWUSR 00200 owner has write permissionS_IXUSR 00100 owner has execute permissionS_IRWXG 00070 group has read, write, and execute permissionS_IRGRP 00040 group has read permissionS_IWGRP 00020 group has write permissionS_IXGRP 00010 group has execute permissionS_IRWXO 00007 others (not in group) have read, write, and execute permissionS_IROTH 00004 others have read permissionS_IWOTH 00002 others have write permissionS_IXOTH 00001 others have execute permission
譬如,判断文件所有者对该文件是否具有可执行权限,可以通过以下方法测试(假设 st
是
structstat
类型变量):
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 ,否则返回 falseS_ISDIR(m) #判断是不是目录,如果是返回 true ,否则返回 falseS_ISCHR(m) #判断是不是字符设备文件,如果是返回 true ,否则返回 falseS_ISBLK(m) #判断是不是块设备文件,如果是返回 true ,否则返回 falseS_ISFIFO(m) #判断是不是管道文件,如果是返回 true ,否则返回 falseS_ISLNK(m) #判断是不是链接文件,如果是返回 true ,否则返回 falseS_ISSOCK(m) #判断是不是套接字文件,如果是返回 true ,否则返回 false
5.2.3 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
小计中对此有详细介绍。
structstat 结构体中包含了三个文件相关的时间属性,但这里得到的仅仅只是以秒+
微秒为单位的时间值,对于我们来说,并不利用查看,我们一般喜欢的是“
2020-10-10 18:30:30
”这种形式表示的时间,直观、明了,那有没有办法通过秒来得到这种形式表达的时间呢?答案当然是可以,譬如可以通过 localtime()/localtime_r()
或者
strftime()
来得到更利于我们查看的时间表达方式,关于这些函数的介绍以及使用方法在 7.2.4
小节有详细说明。
5.2.4 练习
(1)获取文件的 inode 节点编号以及文件大小,并将它们打印出来。
#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);
}
(2)获取文件的类型,判断此文件对于其它用户(Other)是否具有可读可写权限。
#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)获取文件的时间属性,包括文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后被改变的时间,并使用字符串形式将其打印出来,包括时间和日期、表示形式自定。
#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 文件的这些时间属性,对比程序打印出来是否正确:

5.3 fstat 和 lstat 函数
除了 stat
函数之外,还可以使用
fstat
和
lstat
两个系统调用来获取文件属性信息。fstat
、
lstat
与
stat
的作用一样,但是参数、细节方面有些许不同。
5.3.1 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");
printf("file size: %ld bytes\n"
"inode number: %ld\n", file_stat.st_size,
file_stat.st_ino);
printf("st_dev: %ld \n"
"st_mtime: %ld\n", file_stat.st_dev,
file_stat.st_mtime);
close(fd);
exit(ret);
}
5.3.2 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
函数一样,使用方法也一样,这里不再重述!
5.4 文件属主
Linux 是一个多用户操作系统,系统中一般存在着好几个不同的用户,而
Linux
系统中的每一个文件都有一个与之相关联的用户和用户组,通过这个信息可以判断文件的所有者和所属组。
文件所有者表示该文件属于“谁”,也就是属于哪个用户。一般来说文件在创建时,其所有者就是创建该文件的那个用户。譬如,当前登录用户为 dt,使用
touch
命令创建了一个文件,那么这个文件的所有者就是 dt
;同理,在程序中调用
open
函数创建新文件时也是如此,执行该程序的用户是谁,其文件所有者便是谁。
文件所属组则表示该文件属于哪一个用户组。在 Linux
中,系统并不是通过用户名或用户组名来识别不同的用户和用户组,而是通过 ID
。
ID
就是一个编号,
Linux
系统会为每一个用户或用户组分配一个
ID
,将用户名或用户组名与对应的 ID
关联起来,所以系统通过用户
ID
(
UID
)或组
ID
(
GID
)就可以识别出不同的用户和用户组。
譬如使用
ls
命令或
stat
命令便可以查看到文件的所有者和所属组,如下所示:

由上图可知,test.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
用于文件访问权限检查,详情请查看
5.4.1
小节内容。
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权限。若无特别指出,文中的描述均为此意!
5.4.2 chown 函数
chown 是一个系统调用,该系统调用可用于改变文件的所有者(用户
ID
)和所属组(组
ID
)。其实在Linux 系统下也有一个
chown
命令,该命令的作用也是用于改变文件的所有者和所属组,譬如将
testApp.c文件的所有者和所属组修改为 root
:
sudo chown root:root testApp.c

可以看到,通过该命令确实可以改变文件的所有者和所属组,这个命令内部其实就是调用了 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);
}
在运行测试代码之前,先使用了 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);
我们可以在上面的示例代码
中加入打印用户
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 函数就不会报错。
在实际应用编程中,此系统调用被用到的概率并不多,但是理论性知识还是得知道。
5.4.3 fchown 和 lchown 函数
这两个同样也是系统调用,作用与 chown
函数相同,只是参数、细节方面有些许不同。fchown()、
lchown() 这两个函数与 chown()
的区别就像是
fstat()
、
lstat()
与
stat
的区别,本小节就不再重述这种问题了,如果大家对此还不清楚,可以看 5.3
小节,具体使用
fchown
、
lchown
还是
chown
,看情况而定。
5.5 文件访问权限
struct stat 结构体中的
st_mode
字段记录了文件的访问权限位。当提及到文件时,指的是前面给大家介绍的任何类型的文件,并不仅仅指的是普通文件;所有文件类型(目录、设备文件)都有访问权限(access permission),可能有很多人认为只有普通文件才有访问权限,这是一种误解!
5.5.1 普通权限和特殊权限
文件的权限可以分为两个大类,分别是普通权限和特殊权限(也可称为附加权限)。普通权限包括对文件的读、写以及执行,而特殊权限则包括一些对文件的附加权限,譬如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 bitS_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.2 目录权限
前面我们一直谈论的都是文件的读、写、执行权限,那对于创建文件、删除文件等这些操作难道就不需要相应的权限了吗?事实并不如此,譬如:有时删除文件或创建文件也会提示"权限不够
"。
那说明删除文件、创建文件这些操作也是需要相应权限的,那这些权限又是从哪里获取的呢?答案就是目录。目录(文件夹)在 Linux 系统下也是一种文件,拥有与普通文件相同的权限方案(S/U/G/O),只是这些权限的含义另有所指。
目录的读权限:可列出(譬如:通过 ls 命令)目录之下的内容(即目录下有哪些文件)。 |
目录的写权限:可以在目录下创建文件、删除文件。 |
目录的执行权限:可访问目录下的文件,譬如对目录下的文件进行读、写、执行等操作。 |
拥有对目录的读权限,用户只能查看目录中的文件列表。
只有读权限时,可以查看到目录下有哪些文件、显示出文件的名称;但是会看到上面打印出了一些"
权限不够
"
信息,这是因为
Ubuntu
发行版对
ls
命令做了别名处理,执行
ls
命令的时候携带了一些选项,而这些选项会访问文件的一些信息,所以导致出现"
权限不够
"
问题,这也说明,只拥有读权限、是没法访问目录下的文件的;为了确保使用的是 ls
命令本身,执行时需要给出路径的完整路径
/bin/ls。
要想访问目录下的文件,譬如查看文件的 inode
节点、大小、权限等信息,还需要对目录拥有执行权限。
反之,若拥有对目录的执行权限、而无读权限,只要知道目录内文件的名称,仍可对其进行访问,但不能列出目录下的内容(即目录下包含的其它文件的名称)。
要想在目录下创建文件或删除原有文件,需要同时拥有对该目录的执行和写权限。
所以由此可知,如果需要对文件进行读、写或执行等操作,不光是需要拥有该文件本身的读、写或执行权限,还需要拥有文件所在目录的执行权限。
5.5.3 检查文件权限 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("%s: 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);
}
5.5.4 修改文件权限 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-rw-r--
),程序执行完成之后,再次查看文件权限为
rwxrwxrwx
(0777),修改成功!
5.5.5
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
是进程自身的一种属性、
A
进程的
umask
与
B
进程的
umask
无关(父子进程关系除外)。在 shell
终端下可以使用
umask
命令设置
shell
终端的
umask
值,但是该
shell
终端关闭之后、再次打开一个终端,新打开的终端将与之前关闭的终端并无任何瓜葛!
5.6 文件的时间属性
前面给大家介绍了 3
个文件的时间属性:文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后被改变的时间,分别记录在 struct stat
结构体的
st_atim
、
st_mtim
以及
st_ctim
变量中,如下所示:

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

5.6.1 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
文件的访问时间和内容修改时间均被更改为当前时间了(大家可以使用 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);
}
5.7 符号链接(软链接)与硬链接
在 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 号,所以也就是意味着它们之间有着不同的数据块,但是软链接文件的数据块中存储的是源文件的路径名,链接文件可以通过这个路径找到被链接的源文件,它们之间类似于一种“主从”关系,当源文件被删除之后,软链接文件依然存在,但此时它指向的是一个无效的文件路径,这种链接文件被称为悬空链接。


介绍完它们之间的区别之后,大家可能觉得硬链接相对于软链接来说有较大的优势,其实并不是这样,对于硬链接来说,存在一些限制情况,如下:
不能对目录创建硬链接(超级用户可以创建,但必须在底层文件系统支持的情况下)。
|
硬链接通常要求链接文件和源文件位于同一文件系统中。
|
而软链接文件的使用并没有上述限制条件,优点如下所示:
可以对目录创建软链接;
|
可以跨越不同文件系统;
|
可以对不存在的文件创建软链接。
|
5.7.1 创建链接文件
在 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);
}
创建软链接 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) {
perror("symlink error");
exit(-1);
}
exit(0);
}
5.7.2 读取软链接文件
前面给大家介绍到,软链接文件数据块中存储的是被链接文件的路径信息,那如何读取出软链接文件中存储的路径信息呢?大家认为使用 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);
}
5.8 目录
目录(文件夹)在 Linux
系统也是一种文件,是一种特殊文件,同样可以使用前面给大家介绍
open
、read 等这些系统调用以及
C
库函数对其进行操作,但是目录作为一种特殊文件,并不适合使用前面介绍的文件 I/O
方式进行读写等操作。在
Linux
系统下,会有一些专门的系统调用或
C
库函数用于对文件夹进行操作,譬如:打开、创建文件夹、删除文件夹、读取文件夹以及遍历文件夹中的文件等,那么本小节将向大家介绍目录相关的知识内容。
5.8.1 目录存储形式
3.1 小节中给大家介绍了普通文件的管理形式或存储形式,本小节聊一聊目录这种特殊文件在文件系统中的
存储形式,其实目录在文件系统中的存储方式与常规文件类似,常规文件包括了 inode
节点以及文件内容数据存储块(block
)
;但对于目录来说,其存储形式则是由
inode
节点和目录块所构成,目录块当中记录了有哪些文件组织在这个目录下,记录它们的文件名以及对应的 inode
编号。
其存储形式如下图所示:

目录块当中有多个目录项(或叫目录条目),每一个目录项(或目录条目)都会对应到该目录下的某一个文件,目录项当中记录了该文件的文件名以及它的 inode 节点编号,所以通过目录的目录块便可以遍历找到该目录下的所有文件以及所对应的 inode
节点。
所以对此总结如下:
普通文件由
inode
节点和数据块构成
|
目录由
inode
节点和目录块构成
|
5.8.2 创建和删除目录
使用 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 函数测试
通过 mkdir
函数在当前目录下创建了一个目录
new_dir
,并将其权限设置为
0755
(八进制)。
#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);
}
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);
}
5.8.3 打开、读取以及关闭目录
打开、读取、关闭一个普通文件可以使用 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);
}
5.8.4 进程的当前工作目录
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);
}

5.9 删除文件
前面给大家介绍了如何删除一个目录,使用 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);
}
使用 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);
}
5.10 文件重命名
本小节给大家介绍 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);
}
使用 rename
进行文件重命名之后,其
inode
号并未改变。
5.11 总结
本章所介绍的内容比较多,主要是围绕文件属性以及目录展开的一系列相关话题,本章开头先给大家介绍 Linux 系统下的
7
种文件类型,包括普通文件、目录、设备文件(字符设备文件、块设备文件)、符号链接文件(软链接文件)、管道文件以及套接字文件。
接着围绕 stat
系统调用,详细给大家介绍了
struct stat
结构体中的每一个成员,这使得我们对
Linux
下文件的各个属性都有所了解。接着分别给大家详细介绍了文件属主、文件访问权限、文件时间戳、软链接与硬链接以及目录等相关内容,让大家知道在应用编程中如何去修改文件的这些属性以及它们所需要满足的条件。
至此,本章内容到这里就结束了,相信大家已经学习到了不少知识内容,大家加油!