文件类型

正所谓Linux下皆文件,系统下一共分为7种文件:

①普通文件

普通文件(regular file)在 Linux 系统下是最常见的,譬如文本文件、二进制文件,我们编写的源代码文件这些都是普通文件。

普通文件分为文本文件和二进制文件。

文本文件:文件中的内容是由文本构成的,所谓文本指的是 ASCII 码字符。文件中的内容其本质上都是数字。

二进制文件:二进制文件中存储的本质上也是数字,只不过对于二进制文件来说,这些数字并不是文本字符编码,而是真正的数字。譬如 Linux 系统下的可执行文件、C 代码编译之后得到的.o 文件、.bin 文件等都是二进制件。

②目录文件

目录(directory)就是文件夹,文件夹在 Linux 系统中也是一种文件,是一种特殊文件。

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

Linux 系统下,一切皆文件,也包括各种硬件设备。设备文件(字符设备文件、块设备文件)对应的是硬件设备,在 Linux 系统中,硬件设备会对应到一个设备文件,应用程序通过对设备文件的读写来操控、使用硬件设备。

Linux 系统中,可将硬件设备分为字符设备和块设备,所以就有了字符设备文件和块设备文件两种文件类型。虽然有设备文件,但是设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护,当系统关机时,设备文件都会消失;字符设备文件一般存放在 Linux 系统/dev/目录下。

④符号链接文件

符号链接文件(link)类似于 Windows 系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另一个文件路径,当对符号链接文件进行操作时,系统根据情况会对这个操作转移到它指向的文件上去,而不是对它本身进行操作。

⑤管道文件

管道文件(pipe)主要用于进程间通信。

⑥套接字文件

套接字文件(socket)也是一种进程间通信的方式,与管道文件不同的是,它们可以在不同主机上的进程间通信,实际上就是网络通信。

1
$ stat test_file			##可以查看文件属性

这个命令内部就是通过调用 stat()函数来获取文件属性的,stat 函数是 Linux 中的系统调用,用于获取文件相关的信息,函数原型如下所示

1
2
3
4
#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 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct stat
{
dev_t st_dev; /* 文件所在设备的 ID */
ino_t st_ino; /* 文件对应 inode 节点编号 */
mode_t st_mode; /* 文件对应的模式 */
nlink_t st_nlink; /* 文件的链接数 */
uid_t st_uid; /* 文件所有者的用户 ID */
gid_t st_gid; /* 文件所有者的组 ID */
dev_t st_rdev; /* 设备号(指针对设备文件) */
off_t st_size; /* 文件大小(以字节为单位) */
blksize_t st_blksize; /* 文件内容存储的块大小 */
blkcnt_t st_blocks; /* 文件内容所占块数 */
struct timespec st_atim; /* 文件最后被访问的时间 */
struct timespec st_mtim; /* 文件内容最后被修改的时间 */
struct timespec st_ctim; /* 文件状态最后被改变的时间 */
};

st_mode变量

st_mode 是 structstat 结构体中的一个成员变量,是一个 32 位无符号整形数据,该变量记录了文件的类型、文件的权限这些信息。

这些 bit 位表达内容与 open 函数的 mode 参数相对应,这里不再重述。同样,在 mode 参数中表示权限的宏定义,在这里也是可以使用的,这些宏定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

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

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

struct timespec结构体

该结构体定义在头文件中,是 Linux 系统中时间相关的结构体。

1
2
3
4
5
struct timespec
{
time_t tv_sec; /* 秒 */
syscall_slong_t tv_nsec; /* 纳秒 */
};

fstat和lstat函数

fstat函数

fstat 与 stat 区别在于,stat 是从文件名出发得到文件属性信息,不需要先打开文件;而 fstat 函数则是从文件描述符出发得到文件属性信息,所以使用 fstat 函数之前需要先打开文件得到文件描述符。

1
2
3
4
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int fstat(int fd, struct stat *buf);

lstat 函数

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

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

文件属主

Linux 是一个多用户操作系统,系统中一般存在着好几个不同的用户,而 Linux 系统中的每一个文件都有一个与之相关联的用户和用户组,通过这个信息可以判断文件的所有者和所属组。

文件所有者表示该文件属于“谁”,也就是属于哪个用户。一般来说文件在创建时,其所有者就是创建该文件的那个用户。文件所属组则表示该文件属于哪一个用户组。在 Linux 中,系统并不是通过用户名或用户组名来识别不同的用户和用户组,而是通过 ID。ID 就是一个编号,Linux 系统会为每一个用户或用户组分配一个 ID,将用户名或用户组名与对应的 ID 关联起来,所以系统通过用户 ID(UID)或组 ID(GID)就可以识别出不同的用户和用组。

有效用户 ID 和有效组 ID

这是进程所持有的概念,对于文件来说,并无此属性!有效用户 ID 和有效组 ID 是站在操作系统的角度,用于给操作系统判断当前执行该进程的用户在当前环境下对某个文件是否拥有相应的权限。

当进行权限检查时,并不是通过进程的实际用户和实际组来参与权限检查的,而是通过有效用户和有效组来参与文件权限检查。

chown函数

chown 是一个系统调用,该系统调用可用于改变文件的所有者(用户 ID)和所属组(组 ID)。其实在Linux 系统下也有一个 chown 命令,该命令的作用也是用于改变文件的所有者和所属组,

1
$ sudo chown root:root testApp.c			##该命令的作用也是用于改变文件的所有者和所属组

chown函数原型

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

函数参数和返回值如下所示:
pathname:用于指定一个需要修改所有者和所属组的文件路径。
owner:将文件的所有者修改为该参数指定的用户(以用户 ID 的形式描述);
group:将文件的所属组修改为该参数指定的用户组(以用户组 ID 的形式描述);
返回值:成功返回 0;失败将返回-1,兵并且会设置 errno。

有以下两个限制条件:

⚫ 只有超级用户进程能更改文件的用户 ID;
⚫ 普通用户进程可以将文件的组 ID 修改为其所从属的任意附属组 ID,前提条件是该进程的有效用户 ID 等于文件的用户 ID;而超级用户进程可以将文件的组 ID 修改为任意值。

eg:

1
2
3
4
5
6
7
8
9
10
11
#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);
}

在 Linux 系统下,可以使用 getuid 和 getgid 两个系统调用分别用于获取当前进程的用户 ID 和用户组ID,这里说的进程的用户 ID 和用户组 ID 指的就是进程的实际用户 ID 和实际组 ID。

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

eg:

1
2
3
4
5
6
7
8
9
10
11
12
#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);
}

文件访问权限

struct stat 结构体中的 st_mode 字段记录了文件的访问权限位。当提及到文件时,并不仅仅指的是普通文件;所有文件类型(目录、设备文件)都有访问权限(access permission)。

文件的权限可以分为两个大类,分别是普通权限和特殊权限(也可称为附加权限)。

1
$ ls-l							##查看访问权限

-rwxrwxr-x

-表示文件类型,前三位rwx为所有者权限,中间三位为同组用户权限,后三位为其它用户权限。

r表示具有读权限;w表示具有写权限;x表示具有执行权限;-表示没有权限

当进程每次对文件进行读、写、执行等操作时,内核就会对文件进行访问权限检查,以确定该进程对文件是否拥有相应的权限。而对于进程来说,参与文件权限检查的是进程的有效用户、有效用户组以及进程的附属组用户。

如何判断权限,首先要搞清楚该进程对于需要进行操作的文件来说是属于哪一类“角色”:
⚫ 如果进程的有效用户 ID 等于文件所有者 ID(st_uid),意味着该进程以文件所有者的角色存在;
⚫ 如果进程的有效用户 ID 并不等于文件所有者 ID,意味着该进程并不是文件所有者身份;但是进程
的有效用户组 ID 或进程的附属组 ID 之一等于文件的组 ID(st_gid),那么意味着该进程以文件所
属组成员的角色存在,也就是文件所属组的同组用户成员。
⚫ 如果进程的有效用户 ID 不等于文件所有者 ID、并且进程的有效用户组 ID 或进程的所有附属组 ID
均不等于文件的组 ID(st_gid),那么意味着该进程以其它用户的角色存在。
⚫ 如果进程的有效用户 ID 等于 0(root 用户),则无需进行权限检查,直接对该文件拥有最高权限。

宏定义

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

譬如通过 st_mode 变量判断文件是否设置了 set-user-ID 位权限,代码如下:

1
2
3
4
5
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),意味着该进程直接获取了文件所属组成员的权限、以文件所属组成员的身份操作该文件。

目录权限

⚫ 目录的读权限:可列出(譬如:通过 ls 命令)目录之下的内容(即目录下有哪些文件)。
⚫ 目录的写权限:可以在目录下创建文件、删除文件。
⚫ 目录的执行权限:可访问目录下的文件,譬如对目录下的文件进行读、写、执行等操作。

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

检查文件权限

文件的权限检查不只关于文件本身的权限,还需要涉及到文件所在目录的权限,只有同时都满足了,才能通过操作系统的权限检查,进而才可以对文件进行相关操作。可以通过access来检查执行进程的用户是否对该文件拥有应的权限。

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

pathname:需要进行权限检查的文件路径。
mode:该参数可以取以下值:
pathname:需要进行权限检查的文件路径。
mode:该参数可以取以下值:
⚫ F_OK:检查文件是否存在
⚫ R_OK:检查是否拥有读权限
⚫ W_OK:检查是否拥有写权限
⚫ X_OK:检查是否拥有执行权限

返回值:检查项通过则返回 0,表示拥有相应的权限并且文件存在;否则返回-1,如果多个检查项组合在一起,只要其中任何一项不通过都会返回-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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);
}

修改文件权限

在 Linux 系统下,可以使用 chmod 命令修改文件权限,该命令内部实现方法其实是调用了 chmod 函数。

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

pathname:需要进行权限修改的文件路径,若该参数所指为符号链接,实际改变权限的文件是符号链接所指向的文件,而不是符号链接文件本身。
mode:该参数用于描述文件权限,与 open 函数的第三个参数一样,这里不再重述,可以直接使用八进制数据来描述,也可以使用相应的权限宏(单个或通过位或运算符” | “组合)。
返回值:成功返回 0;失败返回-1,并设置 errno。

fchmod()与 chmod()的区别在于使用了文件描述符来代替文件路径,就像是 fstat 与 stat 的区别。

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

umask函数

1
$ umask							##查看/设置权限掩码

权限掩码主要用于对新建文件的权限进行屏蔽。权限掩码的表示方式与文件权限的表示方式相同,但是需要去除特殊权限位,umask 不能对特殊权限位进行屏蔽。

umask 权限掩码是进程的一种属性,用于指明该进程新建文件或目录时,应屏蔽哪些权限位。进程的umask 通常继承至其父进程,譬如在 Ubuntu shell终端下执行的应用程序,它的 umask 继承至该 shell 进程。

函数原型

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

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

文件的时间属性

文件最后被访问的时间、文件内容最后被修改的时间以及文件状态最后被改变的时间,分别记录在 struct stat 结构体的 st_atim、st_mtim 以及 st_ctim 变量中。

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

⚫ 文件最后被访问的时间:访问指的是读取文件内容,文件内容最后一次被读取的时间,譬如使用read()函数读取文件内容便会改变该时间属性;
⚫ 文件内容最后被修改的时间:文件内容发生改变,譬如使用 write()函数写入数据到文件中便会改变该时间属性;
⚫ 文件状态最后被改变的时间:状态更改指的是该文件的 inode 节点最后一次被修改的时间,譬如更改文件的访问权限、更改文件的用户 ID、用户组 ID、更改链接数等,但它们并没有更改文件的实际内容,也没有访问(读取)文件内容。inode 中包含了很多文件信息,譬如:文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(时间属性)、文件数据存储的 block(块)等,所以由此可知,状态的更改指的就是 inode 节点内容的更改。譬如 chmod()、chown()等
这些函数都能改变该时间属性。

列出了一些系统调用或 C 库函数对文件时间属性的影响,有些操作并不仅仅只会影响文件本身的时间属性,还会影响到其父目录的相关时间属性。

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

utime()函数原型如下所示:

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

filename:需要修改时间属性的文件路径。
times:将时间属性修改为该参数所指定的时间值,times 是一个 struct utimbuf 结构体类型的指针,如果times 参数设置为 NULL,则会将文件的访问时间和修改时间设置为系统当前时间。
返回值:成功返回值 0;失败将返回-1,并会设置 errno。

1
2
3
4
5
struct utimbuf 
{
time_t actime; /* 访问时间 */
time_t modtime; /* 内容修改时间 */
};

该结构体中包含了两个 time_t 类型的成员,分别用于表示访问时间和内容修改时间,time_t 类型其实就是 long int 类型。

同样对于文件来说,时间属性也是文件非常重要的属性之一,对文件时间属性的修改也不是任何用户都可以随便修改的,只有以下两种进程可对其进行修改:
⚫ 超级用户进程(以 root 身份运行的进程)。
⚫ 有效用户 ID 与该文件用户 ID(文件所有者)相匹配的进程。
⚫ 在参数 times 等于 NULL 的情况下,对文件拥有写权限的进程。
除以上三种情况之外的用户进程将无法对文件时间戳进行修改。

utimes函数

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

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

1
2
3
4
5
struct timeval 
{
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 */
};

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

futimens()、utimensat()函数是两个系统调用由于显示修改文件时间。

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

1
2
3
#include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);

fd:文件描述符。
times:将时间属性修改为该参数所指定的时间值,times 指向拥有 2 个 struct timespec 结构体类型变量的数组,数组共有两个元素,第一个元素用于指定访问时间,第二个元素用于指定内容修改时间。
返回值:成功返回 0;失败将返回-1,并设置 errno。

该函数的时间戳可以按下列 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(文件所有者)相匹配的进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#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 (ret == -1)
{
printf("Error: %s file does not exist!\n", MY_FILE);
exit(-1);
}
/* 打开文件 */
fd = open(MY_FILE, O_RDONLY);
if (fd == -1)
{
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 (ret == -1)
{
perror("futimens error");
goto err;
}
err:
close(fd);
exit(ret);
}

utimensat()函数功能也是一样的,具体可以使用man 2 utimensat查询用法

符号链接与硬链接

在 Linux 系统中有两种链接文件,分为软链接(也叫符号链接)文件和硬链接文件,软链接文件也就是前面给大家的 Linux 系统下的七种文件类型之一,其作用类似于 Windows 下的快捷方式。硬链接两个文件相互影响。

复习一下shell

1
2
$ln 源文件 链接文件			##硬链接
$ln-s 源文件 链接文件 ##软链接

使用 ln 命令创建的两个硬链接文件与源文件 test_file 都拥有相同的 inode 号,既然inode 相同,也就意味着它们指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已,创建出来的硬链接文件与源文件对文件系统来说是完全平等的关系。

当为文件每创建一个硬链接,inode 节点上的链接数就会加一,每删除一个硬链接,inode 节点上的链接数就会减一,直到为 0,inode 节点和对应的数据块才会被文件系统所回收,也就意味着文件已经从文件系统中被删除了。

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

⚫ 不能对目录创建硬链接(超级用户可以创建,但必须在底层文件系统支持的情况下)。
⚫ 硬链接通常要求链接文件和源文件位于同一文件系统中。
而软链接文件的使用并没有上述限制条件,优点如下所示:
⚫ 可以对目录创建软链接;
⚫ 可以跨越不同文件系统;
⚫ 可以对不存在的文件创建软链接。

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

oldpath:用于指定被链接的源文件路径,应避免 oldpath 参数指定为软链接文件,为软链接文件创建硬链接没有意义,虽然并不会报错。
newpath:用于指定硬链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误。
返回值:成功返回 0;失败将返回-1,并且会设置 errno。

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

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

读取软链接文件

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

pathname:需要读取的软链接文件路径。只能是软链接文件路径,不能是其它类型文件,否则调用函
数将报错。
buf:用于存放路径信息的缓冲区。
bufsiz:读取大小,一般读取的大小需要大于链接文件数据块中存储的文件路径信息字节大小。
返回值:失败将返回-1,并会设置 errno;成功将返回读取到的字节数

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

pathname:需要读取的软链接文件路径。只能是软链接文件路径,不能是其它类型文件,否则调用函数将报错。
buf:用于存放路径信息的缓冲区。
bufsiz:读取大小,一般读取的大小需要大于链接文件数据块中存储的文件路径信息字节大小。
返回值:失败将返回-1,并会设置 errno;成功将返回读取到的字节数。

目录

目录存储形式

目录这种特殊文件在文件系统中的存储形式,其实目录在文件系统中的存储方式与常规文件类似,常规文件包括了 inode 节点以及文件内容数据存储块(block),但对于目录来说,其存储形式则是由 inode 节点和目录块所构成,目录块当中记录了有哪些文件组织在这个目录下,记录它们的文件名以及对应的 inode 编号。

目录块当中有多个目录项(或叫目录条目),每一个目录项(或目录条目)都会对应到该目录下的某一个文件,目录项当中记录了该文件的文件名以及它的 inode 节点编号,所以通过目录的目录块便可以遍历找到该目录下的所有文件以及所对应的 inode 节点。
所以对此总结如下:
⚫ 普通文件由 inode 节点和数据块构成
⚫ 目录由 inode 节点和目录块构成

创建删除目录

mkdir函数

1
2
3
#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 参数指定了新目录的权限,目录拥有与普通文件相同的权限位,但是其表示的含义与普通文件却有不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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

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

pathname:需要删除的目录对应的路径名,并且该目录必须是一个空目录,也就是该目录下只有.和..这两个目录项;pathname 指定的路径名不能是软链接文件,即使该链接文件指向了一个空目录。
返回值:成功返回 0;失败将返回-1,并会设置 errno。

打开、读取以及关闭目录

opendir()函数用于打开一个目录,并返回指向该目录的句柄,供后续操作使用。

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

name:指定需要打开的目录路径名,可以是绝对路径,也可以是相对路径。
返回值:成功将返回指向该目录的句柄,一个 DIR 指针(其实质是一个结构体指针),其作用类似于
open函数返回的文件描述符fd,后续对该目录的操作需要使用该DIR指针变量;若调用失败,则返回NULL。

**readdir()**用于读取目录,获取目录下所有文件的名称以及对应 inode 号。

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

dirp:目录句柄 DIR 指针。
返回值:返回一个指向 struct dirent 结构体的指针,该结构体表示 dirp 指向的目录流中的下一个目录条目。在到达目录流的末尾或发生错误时,它返回 NULL。

“流”这个概念是动态的,而不是静态的。编程当中提到这个概念,一般都是与 I/O 相关,所以也经常叫做 I/O 流;但对于目录这种特殊文件来说,这里将目录块中存储的数据称为目录流,存储了一个一个的目录项(目录条目)。

1
2
3
4
5
6
7
8
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]; /* 文件名 */
};

每调用一次 readdir(),就会从 drip 所指向的目录流中读取下一条目录项(目录条目),并返回 struct dirent结构体指针,指向经静态分配而得的 struct dirent 类型结构,每次调用 readdir()都会覆盖该结构。一旦遇到目录结尾或是出错,readdir()将返回 NULL,针对后一种情况,还会设置 errno 以示具体错误。

那如何区别究竟是到了目录末尾还是出错了呢,可通过如下代码进行判断:

1
2
3
4
5
6
7
8
9
10
11
error = 0;
direntp = readdir(dirp);
if (NULL == direntp)
{
if (0 != error)
{
/* 出现了错误 */
} else {
/* 已经到了目录末尾 */
}
}

rewinddir()可将目录流重置为目录起点。

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

dirp:目录句柄。
返回值:无返回值。

关闭目录close()函数用于关闭处于打开状态的目录,同时释放他所用的资源。

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

dirp:目录句柄。
返回值:成功返回 0;失败将返回-1,并设置 errno。

进程的当前工作目录

Linux 下的每一个进程都有自己的当前工作目录(current working directory),当前工作目录是该进程解析、搜索相对路径名的起点。

一般情况下,运行一个进程时、其父进程的当前工作目录将被该进程所继承,成为该进程的当前工作目录。可通过 getcwd 函数来获取进程的当前工作目录,如下所示:

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

buf:getcwd()将内含当前工作目录绝对路径的字符串存放在 buf 缓冲区中。
size:缓冲区的大小,分配的缓冲区大小必须要大于字符串长度,否则调用将会失败。
返回值:如果调用成功将返回指向 buf 的指针,失败将返回 NULL,并设置 errno。

改变当前工作目录

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

path:将进程的当前工作目录更改为 path 参数指定的目录,可以是绝对路径、也可以是相对路径,指定的目录必须要存在,否则会报错。
fd:将进程的当前工作目录更改为 fd 文件描述符所指定的目录(譬如使用 open 函数打开一个目录)。
返回值:成功均返回 0;失败均返回-1,并设置 errno。

删除文件

使用unlink函数删除一个文件

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

pathname:需要删除的文件路径,可使用相对路径、也可使用绝对路径,如果 pathname 参数指定的文件不存在,则调用 unlink()失败。
返回值:成功返回 0;失败将返回-1,并设置 errno。

unlink()系统调用实质上是移除 pathname 参数指定的文件路径对应的目录项(从其父级目录中移除该目录项),并将文件的 inode 链接计数将 1,如果该文件还有其它硬链接,则任可通过其它链接访问该文件的数据;只有当链接计数变为 0 时,该文件的内容才可被删除。另一个条件也会阻止删除文件的内容—只要有进程打开了该文件,其内容也不能被删除。关闭一个文件时,内核会检查打开该文件的进程个数,如果这个计数达到 0,内核再去检查其链接计数,如果链接计数也是 0,那么就删除该文件对应的内容(也就是文件对应的 inode 以及数据块被回收,如果一个文件存在多个硬链接,删除其中任何一个硬链接,其inode 和数据块并没有被回收,还可通过其它硬链接访问文件的数据)。

使用 remove 函数删除文件

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

pathname:需要删除的文件或目录路径,可以是相对路径、也可是决定路径。
返回值:成功返回 0;失败将返回-1,并设置 errno。
pathname 参数指定的是一个非目录文件,那么 remove()去调用 unlink(),如果 pathname 参数指定的是一个目录,那么 remove()去调用 rmdir()。

文件重命名

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

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

返回值:成功返回 0;失败将返回-1,并设置 errno。

根据 oldpath、newpath 的不同,有以下不同的情况需要进行说明:
⚫ 若 newpath 参数指定的文件或目录已经存在,则将其覆盖;
⚫ 若 newpath 和 oldpath 指向同一个文件,则不发生变化(且调用成功)。
⚫ rename()系统调用对其两个参数中的软链接均不进行解引用。如果 oldpath 是一个软链接,那么将重命名该软链接;如果 newpath 是一个软链接,则会将其移除、被覆盖。
⚫ 如果 oldpath 指代文件,而非目录,那么就不能将 newpath 指定为一个目录的路径名。要想重命名
一个文件到某一个目录下,newpath 必须包含新的文件名。
⚫ 如果 oldpath 指代为一个目录,在这种情况下,newpath 要么不存在,要么必须指定为一个空目录。
⚫ oldpath 和 newpath 所指代的文件必须位于同一文件系统。由前面的介绍,可以得出此结论!
⚫ 不能对.(当前目录)和..(上一级目录)进行重命名。