1、文件类型
Linux 下一切皆文件,文件是 Linux 系统设计思想的核心理念
上一章学习普通文件(文本文件、二进制文件等)文件 I/O 相关的知识内容;虽然在 Linux 系统中大部分文件都是普通文件,但并不仅仅只有普通文件
在 Windows 系统下,操作系统识别文件类型一般是通过文件名后缀来判断
在 Linux 系统下,并不会通过文件后缀名来识别一个文件的类型,但是文件后缀也要规范、需要根据文件本身的功能属性来添加,为了我们自己方便查看、浏览。
在 Linux 系统下,可以通过 stat 命令或者 ls 命令来查看文件类型
stat 命令非常友好,会直观把文件类型显示出来;对于 ls 命令来说,并没有直观的显示出文件的类型,而是通过符号表示出来
- ' - ':普通文件
- ' d ':目录文件
- ' c ':字符设备文件
- ' b ':块设备文件
- ' l ':符号链接文件
- ' s ':套接字文件
- ' p ':管道文件
1.1 普通文件
如文本文件、二进制文件,我们编写的源代码文件这些都是普通文件,也就是一般意义上的文件。普通文件中的数据存在系统磁盘中,可以访问文件中的内容,文件中的内容以字节为单位进行存储与访问。
普通文件可以分为两大类:文本文件和二进制文件
- 文本文件:文件中的内容是由文本构成的,所谓文本指的是 ASCII 码字符。文件中的内容其本质上都是数字,文本文件的好处就是方便人阅读、浏览以及编写。
- 二进制文件:二进制文件中存储的本质上也是数字,只不过对于二进制文件来说,这些数字并不是 文本字符编码,而是真正的数字。譬如 Linux 系统下的可执行文件、C 代码编译之后得到的.o 文 件、.bin 文件等都是二进制文件。
1.2 目录文件
目录(directory)就是文件夹,文件夹在 Linux 系统中也是一种文件,是一种特殊文件,同样也可以使用 vi 编辑器来打开文件夹
可以看到,文件夹中记录了该文件夹本身的路径以及该文件夹下所存放的文件。文件夹作为一种特殊文件,本身并不适合使用前面文件 I/O 的方式来读写,在 Linux 系统下,会有一些专门的系统调用用于读写文件夹
1.3 字符设备文件和块设备文件
在 Linux 系统中,硬件设备会对应到一个设备文件,应用程序通过对设备文件的读写来操控、使用硬件设备,譬如 LCD 显示屏、串口、音频、按键等
硬件设备分为字符设备和块设备,所以就有了字符设备文件和块设备文件两种文件类型。虽然有设备文件,但是设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护,当系统关机时,设备文件都会消失
字符设备文件一般存放在 Linux 系统/dev/目录下,所以/dev 也称为虚拟文件系统 devfs。
1.4 符号链接文件
符号链接文件(link)类似于 Windows 系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另一个文件路径,当对符号链接文件进行操作时,系统根据情况会对这个操作转移到它指向的文件上去,而不是对它本身进行操作
1.5 管道文件
管道文件(pipe)主要用于进程间通信
1.6 套接字文件
套接字文件(socket)也是一种进程间通信的方式,与管道文件不同的是,它们可以在不同主机上的进程间通信,实际上就是网络通信
2、stat函数
stat 函数是 Linux 中的系统调用,用于获取文件相关的信息
#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 结构体中
- 返回值:成功返回 0;失败返回-1,并设置 error
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; /* 文件状态最后被改变的时间 */
};
2.2 st_mode 变量
struct stat 结构体中的一个成员变量,是一个 32 位无符号整形数据,该变量记录了文件的类型、文件的权限这些信息
对比open函数的mode参数,多了描述文件类型的4个bit位,同样,在 mode 参数中表示权限的宏定义,在这里也是可以使用的
// 譬如,判断文件所有者对该文件是否具有可执行权限,可以通过以下方法测试
if (st.st_mode & S_IXUSR) {
//有权限
} else {
//无权限
}
可以使用 Linux 系统封装好的宏来进行判断文件类型:
S_ISREG(st) //判断是不是普通文件
S_ISDIR(st) //判断是不是目录
S_ISCHR(st) //判断是不是字符设备文件
S_ISBLK(st) //判断是不是块设备文件
S_ISFIFO(st)//判断是不是管道文件
S_ISLNK(st) //判断是不是链接文件
S_ISSOCK(st)//判断是不是套接字文件
2.3 struct timespec 结构体
该结构体定义在<time.h>头文件中,是 Linux 系统中时间相关的结构体
struct timespec
{
time_t tv_sec; // 秒
syscall_slong_t tv_nsec; // 纳秒
};
struct stat 结构体中包含了三个文件相关的时间属性,但仅仅只是以秒+微秒为单位的时间值,可以通过 localtime()/localtime_r()或者 strftime()来得到更利于我们查看的时间表达方式
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
int main()
{
struct stat file_stat;
struct tm *tm_p;
char buf[128];
int ret;
ret = stat("test.txt", &file_stat);
if (ret == -1)
{
perror("stat error");
return -1;
}
tm_p = localtime(&file_stat.st_ctime);
memset(buf, 0, sizeof(buf) / sizeof(buf[0]));
strftime(buf, 128, "%Y-%m-%d %X", tm_p);
printf("file size: %ld bytes\n", file_stat.st_size);
printf("inode number: %ld\n", file_stat.st_ino);
printf("create time: %s\n", buf);
return 0;
}
2.4 fstat 和 lstat
除了 stat 函数之外,还可以使用 fstat 和 lstat 两个系统调用来获取文件属性信息。fstat、lstat 与 stat 的作用一样,但是参数、细节方面有些许不同
fstat 与 stat 区别在于,stat 是从文件名出发得到文件属性信息,不需要先打开文件;而 fstat 函数则是从文件描述符出发得到文件属性信息
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int fstat(int fd, struct stat *buf);
lstat与 stat、fstat 的区别在于,对于符号链接文件,stat、fstat 查阅的是符号链接文件所指向的文件对应的文件属性信息,而 lstat 查阅的是符号链接文件本身的属性信息
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int lstat(const char *pathname, struct stat *buf);
3、文件属主
Linux 是一个多用户操作系统,系统中一般存在着好几个不同的用户,而 Linux 系统中的每一个文件都有一个与之相关联的用户和用户组
3.1 有效用户 ID 和有效组 ID
首先对于有效用户 ID 和有效组 ID 来说,这是进程所持有的概念,对于文件来说,并无此属性!有效用户 ID 和有效组 ID 是站在操作系统的角度,用于给操作系统判断当前执行该进程的用户在当前环境下对某个文件是否拥有相应的权限。
在 Linux 系统中,当进程对文件进行读写操作时,系统首先会判断该进程的有效用户和有效组是否具有对该文件的读写权限
3.2 chown
chown 是一个系统调用,该系统调用可用于改变文件的所有者(用户 ID)和所属组(组 ID)
Linux 系统下也有一个 chown 命令,该命令的作用也是用于改变文件的所有者和所属组(内部其实就是调用了 chown 函数来实现功能的)
#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(用户 ID 或用户组 ID)保持一致即可
限制条件:
- 只有超级用户进程能更改文件的用户 ID;
- 普通用户进程可以将文件的组 ID 修改为其所从属的任意附属组 ID,前提条件是该进程的有效用户 ID 等于文件的用户 ID;而超级用户进程可以将文件的组 ID 修改为任意值。
fchown 和 lchown 函数:作用与 chown 函数相同,只是参数、细节方面有些许不同。与 chown()的区别就像是 fstat()、lstat()与 stat 的区别
4、文件访问权限
struct stat 结构体中的 st_mode 字段记录了文件的访问权限位
4.1 普通权限和特殊权限
文件的权限可以分为两个大类,分别是普通权限和特殊权限限(也可称为附加权限)
普通权限
每个文件都有 9 个普通的访问权限位,可将它们分为 3 类
ls -l显示信息中,前面的一串字符串就描述了该文件的 9 个访问权限以及文件类型
当进程每次对文件进行读、写、执行等操作时,内核就会对文件进行访问权限检查,以确定该进程对文 件是否拥有相应的权限。而文件的权限检查就涉及到了文件的所有者(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 个特殊权限
作用:
- 当进程对文件进行操作的时候、将进行权限检查,如果文件的 set-user-ID 位权限被设置,内核会将进程的有效 ID 设置为该文件的用户 ID(文件所有者 ID),意味着该进程直接获取了文件所有者的权限、以文件所有者的身份操作该文件。
- 当进程对文件进行操作的时候、将进行权限检查,如果文件的 set-group-ID 位权限被设置,内核会将进程的有效用户组 ID 设置为该文件的用户组 ID(文件所属组 ID),意味着该进程直接获取了文件所属组成员的权限、以文件所属组成员的身份操作该文件。
- Sticky 位...
4.2 目录权限
对文件的创建和删除等操作也是需要有权限的
目录(文件夹)在 Linux 系统下也是一种文件,拥有与普通文件相同的权限方案(S/U/G/O),只是 这些权限的含义另有所指
- 目录的读权限:可列出(譬如:通过 ls 命令)目录之下的内容(即目录下有哪些文件)。
- 目录的写权限:可以在目录下创建文件、删除文件。
- 目录的执行权限:可访问目录下的文件,譬如对目录下的文件进行读、写、执行等操作。
要想在目录下创建文件或删除原有文件,需要同时拥有对该目录的执行和写权限。
4.3 文件权限检查
文件的权限检查不单单只讨论文件本身的权限,还需要涉及到文件所在目录的权限,只有同时都满足了,才能通过操作系统的权限检查,进而才可以对文件进行相关操作
可以使用 access 系统调用,检查执行进程的用户是否对该文件拥有相应的权限
#include <unistd.h>
int access(const char *pathname, int mode);
pathname:需要进行权限检查的文件路径
mode:该参数可以取以下值:
- F_OK:检查文件是否存在
- R_OK:检查是否拥有读权限
- W_OK:检查是否拥有写权限
- X_OK:检查是否拥有执行权限
返回值:检查项通过则返回 0,表示拥有相应的权限并且文件存在;否则返回-1,如果多个检查项组合 在一起,只要其中任何一项不通过都会返回-1。
4.4 修改文件权限
在 Linux 系统下,可以使用 chmod 命令修改文件权限,该命令内部实现方法其实是调用了 chmod 函数, chmod 函数是一个系统调用
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
mode:该参数用于描述文件权限,与 open 函数的第三个参数一样,可以直接使用八进 制数据来描述,也可以使用相应的权限宏(单个或通过位或运算符" | "组合)
返回值:成功返回 0;失败返回-1,并设置 errno
#include <sys/stat.h>
int fchmod(int fd, mode_t mode); // 区别在于使用了文件描述符来代替文件路径
4.5 umask 函数
在 Linux 系统中,创建一个新的文件或者目录的时候,这些新的文件或目录都会有默认的访问权限。若用户创建一个文件,则文件的默认访问权限为 -rw-rw-rw-,创建目录的默认权限 drwxrwxrwx。umask 命令与文件和目录的默认访问权限有关,而umask 值则表明了需要从默认权限中去掉哪些权限来成为最终的默认权限值。
譬如:调用 open 函数新建文件时,文件实际的权限并不等于 mode 参数所描述的权限,而是通过如下关系得到实际权限
mode & ~umaskmode 参数指定为 0777,假设 umask 为 0002,那么实际权限为
0777 & (~0002) = 0775
umask命令用于查看/设置权限掩码,权限掩码主要用于对新建文件的权限进行屏蔽
umask函数系统调用:
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
mask:需要设置的权限掩码值,与 open 函数的mode参数类型一样
返回值:返回设置之前的 umask 值,也就是旧的 umask
umask 权限掩码是进程的一种属性,用于指明该进程新建文件或目录时,应屏蔽哪些权限位。进程的 umask 通常继承至其父进程
5、链接
在 Linux 系统中有两种链接文件,分为软链接(也叫符号链接)文件和硬链接文件
软链接文件就是前面的 Linux 系统下的七种文件类型之一,其作用类似于 Windows 下的快捷方式
硬链接在使用的角度上两者没有区别
ln 命令可以为一个文件创建软链接文件或硬链接文件
硬链接:ln 源文件 链接文件
软链接:ln -s 源文件 链接文件
5.1 硬链接
使用 ln 命令创建的两个硬链接文件与源文件 test_file 都拥有相同的 inode 号,既然 inode 相同,也就意味着它们指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已,创建出来的硬链 接文件与源文件对文件系统来说是完全平等的关系
删除一个硬链接文件或源文件,文件所对应的 inode 以及文件内容在磁盘中的数据块不会被系统回收,因为 inode 数据结结构中会记录文件的链接数,这个链接数指的就是硬链接数,每删除一个硬链接,inode 节点上的链接 数就会减一,直到为 0,inode 节点和对应的数据块才会被文件系统所回收
5.2 软连接
软链接文件与源文件有着不同的 inode 号,意味着它们之间有着不同的数据块,但是软链接文件的数据块中存储的是源文件的路径名,链接文件可以通过这个路径找到被链接的源文件,它们之间类似于一种“主从”关系,当源文件被删除之后,软链接文件依然存在,但此时它指向的是一个无效的文件路径,这种链接文件被称为悬空链接
注意:inode 节点中记录的链接数并未将软链接计算在内
5.3 创建链接文件
创建硬链接 link
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
创建软链接 symlink
#include <unistd.h>
int symlink(const char *target, const char *linkpath);
5.4 读取链接文件
软链接文件数据块中存储的是被链接文件的路径信息
不能使用read函数读取,使用 read 函数之前,需要先 open 打开该文件得到文件描述符,但是调用 open 打开一个链接文件本身是不会成功的,因为打开的并不是链接文件本身、而是其指向的文件,所以不能使用 read 来读取
readlink 函数
#include <unistd.h>
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
pathname:需要读取的软链接文件路径。只能是软链接文件路径,不能是其它类型文件,否则调用函 数将报错
buf:用于存放路径信息的缓冲区
bufsiz:读取大小,一般读取的大小需要大于链接文件数据块中存储的文件路径信息字节大小
返回值:失败将返回-1,并会设置 errno;成功将返回读取到的字节数
6、目录
目录(文件夹)在 Linux 系统也是一种文件,是一种特殊文件,有专门的系统调用或 C 库函数用于对文件夹进行 操作,譬如:打开、创建文件夹、删除文件夹、读取文件夹以及遍历文件夹中的文件等
6.1 目录存储形式
目录在文件系统中的存储方式与常规文件类似,常规文件包括inode节点以及文件内容数据存储块(block)
目录其存储形式则是由inode节点和目录块所构成,目录块当中记录了有哪些文件组织在这个目录下,记录它们的文件名以及对应的 inode 编号
6.2 创建和删除目录
使用 open 函数可以创建一个普通文件,但不能用于创建目录文件,在 Linux 系统下,提供了专门用于创建目录 mkdir()以及删除目录 rmdir 相关的系统调用
mkdir
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
mode 参数指定了新目录的权限,目录拥有与普通文件相同的权限位,但是其表示的含义与普通文件却有不同
,查看4.2小节
rmdir
#include <unistd.h>
int rmdir(const char *pathname);
// 需要删除的目录对应的路径名,并且该目录必须是一个空目录,也就是该目录下只有.和..这 两个目录项;pathname 指定的路径名不能是软链接文件,即使该链接文件指向了一个空目录。
6.3 打开、读取、关闭目录
打开、读取、关闭一个普通文件可以使用 open()、read()、close(),而对于目录来说,可以使用 opendir()、 readdir()和 closedir()来打开、读取以及关闭目录
opendir 打开一个目录,并返回指向该目录的句柄,供后续操作使用。属于C库函数
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
/*
* 成功将返回指向该目录的句柄,一个 DIR 指针(其实质是一个结构体指针),其作用类似于 open函数返回的文件描述符fd,后续对该目录的操作需要使用该DIR指针变量;若调用失败,则返回NULL
*/
readdir 读取目录,获取目录下所有文件的名称以及对应 inode 号
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
/*
* 返回一个指向 struct dirent 结构体的指针,该结构体表示 dirp 指向的目录流(将目录块中存储的数据称为目录流)中的下一个目录条目。在到达目录流的末尾或发生错误时,它返回 NULL。
* 每调用一次 readdir(),就会从 drip 所指向的目录流中读取下一条目录项(目录条目),并返回 struct dirent结构体指针,指向经静态分配而得的 struct dirent 类型结构,每次调用 readdir()都会覆盖该结构。一旦遇到目录结尾或是出错,readdir()将返回 NULL,针对后一种情况,还会设置 errno 以示具体错误
* 对于 struct dirent 结构体,我们只需要关注 d_ino 和 d_name 两个字段即可,分别记录了文件的 inode 编 号和文件名
*/
struct dirent {
ino_t d_ino; /* inode 编号 */
char d_name[256]; /* 文件名 */
}
rewinddir 将目录流重置为目录起点,以便对 readdir()的下一次调用将从目录列表中的 第一个文件开始
#include <sys/types.h>
#include <dirent.h>
void rewinddir(DIR *dirp);
closedir 关闭处于打开状态的目录,同时释放它所使用的资源
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
练习:打开目录,并输出下面所有文件的文件名称
#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);
}
6.4 进程的当前工作目录
Linux 下的每一个进程都有自己的当前工作目录,当前工作目录是该进程解析、搜索相对路径名的起点
获取进程的当前工作目录
一般情况下,运行一个进程时其父进程的当前工作目录将被该进程所继承,成为该进程的当前工作目录。可通过getcwd 函数来获取
#include <unistd.h>
/*
* buf:getcwd()将内含当前工作目录绝对路径的字符串存放在 buf 缓冲区中。
* size:缓冲区的大小,分配的缓冲区大小必须要大于字符串长度,否则调用将会失败。
* 返回值:如果调用成功将返回指向 buf 的指针,失败将返回 NULL,并设置 errno。
*/
char *getcwd(char *buf, size_t size);
改变当前工作目录
系统调用 chdir()和 fchdir()可以用于更改进程的当前工作目录
#include <unistd.h>
int chdir(const char *path); // 将进程的当前工作目录更改为 path 参数指定的目录
int fchdir(int fd); // 将进程的当前工作目录更改为 fd 文件描述符所指定的目录
7、删除文件
通过系统调用 unlink()或使用 C 库函数 remove() 可以删除文件
unlink
#include <unistd.h>
int unlink(const char *pathname);
- link 函数,用于创建一个硬链接文件,创建硬链接时,inode 节点上的链接数就会增加
- unlink的作用与 link相反,unlink系统调用用于移除/删除一个硬链接(从其父级目录下删除该目录条目)并将文件的 inode 链接计数减 1。unlink系统调用并不会对软链接进行解引用操作,若 pathname 指定的文件为软链接文件,则删除软链接文件本身,而非软链接所指定的文件
remove
#include <stdio.h>
int remove(const char *pathname);
pathname 参数指定的是一个非目录文件,那么 remove去调用 unlink,如果 pathname 参数指定的是一个目录,那么 remove去调用 rmdir
remove同样不对软链接进行解引用操作
8、重命名文件
rename()既可以对文件进行重命名,又可以将文件移至同 一文件系统中的另一个目录下
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
- 仅操作目录条目,而不移动文件数据(不改变文件 inode 编号、不移动文件数据块中存储的内容)
- 若 newpath 参数指定的文件或目录已经存在,则将其覆盖
- 若 newpath 和 oldpath 指向同一个文件,则不发生变化
- rename()系统调用对其两个参数中的软链接均不进行解引用。如果 oldpath 是一个软链接,那么将重命名该软链接;如果 newpath 是一个软链接,则会将其移除、被覆盖
- 不能对.(当前目录)和..(上一级目录)进行重命名