【Linux文件IO】Linux中文件属性与目录操作的API介绍和用法
Linux中文件属性与目录操作的API介绍和用法
- 一、文件属性
- 1、获取文件属性信息的函数接口
- 2、文件的设备号
- 示例代码:
- 3、类型与权限
- 4、其他简单文件信息
- 二、目录操作
- 1、基本概念
- 2、相关的接口函数
- (1) 打开目录
- (2) 读取目录(重点)
- 示例代码:
- (3) 新建目录
- 示例代码:
- (4) 删除目录
- 示例代码:
- (5) 关闭目录
在应用开发中,经常要获取文件的属性,例如:文件的类型、大小、权限、设备号、最近修改时间等等,比如网络传输文件时,一般都需要先传递文件的属性,等准备妥善了再开始传输文件的真实内容。因此,熟悉文件属性的细节,并熟练获取这些信息的方式至关重要。
一、文件属性
1、获取文件属性信息的函数接口
如下函数可以获取指定文件的属性:
注意:
1.以上三个函数的功能一样,区别如下:
- stat针对文件名获取其信息。
- fstat针对文件描述符获取其信息。
- lstat可以获取软连接文件(快捷方式)本身的属性。
2.这几个函数获取了文件的属性之后,会将这些信息存入一个称为stat的结构体中(与函数同名),该结构体的细节如下:
struct stat
{
dev_t st_dev; // 本文件所在的设备的设备号,适用于非设备文件
ino_t st_ino; // i节点号,相当于身份证号码
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; // 最近状态改变时间,比如修改了文件的权限的时间
};
// 其中,时间戳结构体的细节是:
struct timespec
{
long tv_sec; /* 秒 */
long tv_nsec; /* 纳秒 */
1秒 == 1000毫秒 == 1000 000微妙 == 1000 000 000纳秒
};
2、文件的设备号
struct stat
{
dev_t st_dev; // 本文件所在的设备的设备号,适用于非设备文件
...
dev_t st_rdev; // 本文件的设备号,适用于特殊设备文件
...
...
};
Linux系统为了方便管理,为每一种设备分配了主次设备号,主设备号用来规范设备的类型;次设备号用来规范该种设备在本系统中的序号。设备号是系统资源,在设备被加载的时候分配完毕。
- 以下两个函数,常用来获取主次设备号:
unsigned int major(dev_t dev); // 从 dev 中获取主设备号
unsigned int minor(dev_t dev); // 从 dev 中获取次设备号
- 注意,在结构体stat中有两个类型为 dev_t 的成员。他们分别代表:
- 本文件所在设备的设备号,适用于非设备文件。
- 本文件的设备号,适用于特殊设备文件。
- 特殊设备文件只有设备号的属性,没有文件大小的属性,即st_size是无效的。
- 解析:
- 非设备文件是没有设备号的,此时该文件的 st_rdev 成员是无效的。
- 非设备文件一定是存储某个存储器上的,此时该文件的 st_dev 代表的是其所在存储器的设备号,比如某个硬盘。
- 特殊设备文件指的是类型为字符设备或块设备的文件,比如键盘、鼠标、硬盘、显示器等。
- 特殊设备文件的 st_dev 成员是无效的,只有 st_rdev 有效。
示例代码:
int main(int argc, char **argv)
{
// 获取指定文件的属性信息
struct stat info;
stat(argv[1], &info);
// 是特定的设备文件,那么 st_rdev 有效且 st_size 无效
if(S_ISCHR(info.st_mode) || S_ISBLK(info.st_mode))
{
printf("该文件的主次设备号分别是:%d,%d\n",
major(info.st_rdev),
minor(info.st_rdev));
}
// 不是设备文件,那么 st_dev 有效且 st_size 也有效
else
{
printf("文件大小是:%d", info.st_size);
printf("文件所在设备的主次设备号分别是:%d,%d\n",
major(info.st_dev),
minor(info.st_dev));
}
}
3、类型与权限
struct stat
{
mode_t st_mode; // 文件类型 + 文件权限
...
...
};
在结构体 stat 中,文件的类型和权限并没有分开存储,而是被统一存储到同一个成员 st_mode 中,该成员的内部结构如下所示:
- 关键点:
- st_mode是一个16位的 short 短整型数据。
- 前4位表达文件的类型,由于Linux文件类型总共7种。(0000到1111)
- 中间三位分别是 setuid、setgid 和 stickyBit。
- 后9位表达文件的权限,与三组权限一一对应。
- 判断文件的类型可以用如下宏即可:
S_ISREG(st_mode) is it a regular file? // 是一个普通文件吗?
S_ISDIR(st_mode) directory? // 是一个目录文件吗?
S_ISCHR(st_mode) character device? // 是一个字符设备文件吗?
S_ISBLK(st_mode) block device? // 是一个块设备文件吗?
S_ISFIFO(st_mode) FIFO (named pipe)? // 是一个管道文件吗?(系统编程)
S_ISLNK(st_mode) symbolic link? (Not in POSIX.1-1996.) // 是一个链接文件吗?(快捷方式)
S_ISSOCK(st_mode) socket? (Not in POSIX.1-1996.) // 是一个网络文件吗?(网络编程)
- setuid、setgid(只对普通文件有效)与stickyBit (只针对目录有效)
- 作用解析:
- setuid: 使权得文件的使用者获得文件所有者的临时授。
// ubuntu20.04系统没用
- setgid: 使得文件的使用者获得文件所在目录的所属组的临时授权。
- stickyBit:使得用户只能增加和删除属于自身的文件,不能删除别的用户的文件
// ubuntu20.04系统有用
- setuid: 使权得文件的使用者获得文件所有者的临时授。
- 修改文件setuID示例:
ls -l
-rwxr-xr-x 1 xxx xxx 8520 Dec 18 00:27 a.out
chmod 04755 a.out
ls -l
-rwxr-xr-x 1 xxx xxx 8520 Dec 18 00:27 a.out
使用 setID 解决实际问题的一个典型例子,就是系统中用于修改密码的命令:
ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 59640 Mar 23 2019 /usr/bin/passwd
程序命令 passwd 是属于根用户 root 的,但由于它被设置了setuid,因此以后不管是谁来执行这个程序,在其执行期间都会临时获得 root 的临时授权,这么做是因为修改密码的本质上修改文件 /etc/passwd 的内容,而该文件只有管理员 root 才能修改,设置了 setuid 之后,普通用户既可以通过命令 passwd 来修改此文件,也避免了索取管理员密码的步骤,非常实用。
- 修改目录stickyBit示例:
ls -l
drwxrwxrwx 2 xxx xxx 4096 Dec 20 18:13 dir/
chmod 01777 dir/
ls -l
drwxrwxrwt 2 xxx xxx 4096 Dec 20 18:13 dir/
4、其他简单文件信息
- 文件的所有者与所属组:
struct stat
{
uid_t st_uid; // 文件所有者ID
gid_t st_gid; // 文件所在组ID
...
};
从结构体 stat 中获取的文件所有者和所属组信息中,只有它们的ID号,而没有切确的名称,可以通过以下函数来获取切确的名称:
struct passwd *getpwuid(uid_t uid);
struct group *getgrgid(gid_t gid);
- 文件的尺寸相关信息:
struct stat
{
nlink_t st_nlink; // 文件的别名的数目
off_t st_size; // 文件大小
blksize_t st_blksize; // 标准IO建议的数据块大小
blkcnt_t st_blocks; // 文件占用的数据块个数
...
};
- 关键点:
a.文件别名数目也称为硬链接数目,亦即索引个数,文件系统正是以此来判断某个文件是否可以彻底删除的标记。
b.文件大小就是执行命令 “ls -l” 时所展示大小,包含文件空间的大小。
二、目录操作
1、基本概念
\quad
目录也是一种文件,因此操作流程与普通文件类似,有诸如打开、关闭、定位等概念,但目录是一种特殊的文件,目录存储的数据的最小单位并不是字符,而是目录项
。这使得目录跟普通文件又有区别。
\quad
在Linux文件系统的经典结构中,目录不同于文件夹,目录的本质是索引,文件夹的本质是容器
。在Linux中,目录有几个要点:
- 整个分区被分成两部分,一部分称为i节点域,另一部分称为数据域
- i节点域记录的是整个分区的基本信息,包括分区可用空间和已用空间的管理信息
- 数据域存储文件实际内容数据
- 每一个文件(包括目录本身)拥有一个唯一的标识,称为i节点号,分区使用i节点号管理并索引所有的文件,注意i节点号是分区内部的信息,就像美国的公民ID是美国内部管理信息一样,在中国是无效的,i节点号不能跨分区,这也是为什么使用命令 ln 创建文件别名不能跨分区的原因。
- 目录所存储的数据单元是目录项,目录项指的是结构体
struct dirent{},其内部保存的是文件的名称、i节点号等基本信息,不包含文件具体内容。 - 任何一个目录至少包含两个目录项:.和…
- [.]代表当前目录,[…]代表上一级目录(任意一个文件夹内敲:ls -a即可看到)
- 如果本目录就是根目录,那么[…]也代表本机目录
2、相关的接口函数
(1) 打开目录
#include <dirent.h>
DIR *opendir(const char *name);
返回值:成功 返回DIR指针表示打开的那个目录
失败 NULL
参数:name --》目录的路径名
(2) 读取目录(重点)
struct dirent *readdir(DIR *dirp);
返回值:成功返回存放当前目录中的文件信息
struct dirent {
ino_t d_ino; //节点编号
off_t d_off; //文件偏移位置
unsigned short d_reclen;
unsigned char d_type; //文件类型
DT_BLK 块设备
DT_CHR 字符设备
DT_DIR 目录
DT_FIFO 管道
DT_LNK 软链接
DT_REG 普通文件
DT_SOCK 套接字
DT_UNKNOWN 文件不认识
char d_name[256]; //文件名字
};
失败或者读取目录完毕,会返回NULL
示例代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
int main(int argc, char const *argv[])
{
DIR * dir_ret= opendir("/home/lmr");
struct dirent *dirent;
if (dir_ret == NULL)
{
perror("打开目录失败\n");
return -1;
}
// 循环读取目录
while (1)
{
// 读取目录中的目录项(指的是目录中包含各种类型的文件)
dirent = readdir(dir_ret);
if (dirent == NULL)
break;
if (dirent->d_type == DT_REG)
printf("读取的是普通文件,名字为:%s\n", dirent->d_name);
if (dirent->d_type == DT_DIR)
printf("读取的是子目录(子文件夹),名字为:%s\n", dirent->d_name);
}
closedir(dir_ret);
return 0;
}
(3) 新建目录
int mkdir(const char *pathname, mode_t mode);
返回值:成功 0 失败 -1
参数:pathname --》目录的路径名
mode --》权限0777 0666
表面上写的是0777,但是实际的权限计算公式是: 0777&(~umask)
示例代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
int main(int argc, char const *argv[])
{
// 新建空白目录
// mkdir("./new_dir", 0777);
// 不允许嵌套新建目录--错误写法
// mkdir("./new_dir/123/456/789", 0777);
// 可以这样写--正确写法
mkdir("./new_dir", 0777);
mkdir("./new_dir/123", 0777);
mkdir("./new_dir/123/456", 0777);
mkdir("./new_dir/123/456/789", 0777);
return 0;
}
(4) 删除目录
示例代码:
int main(void)
{
// 在家目录下创建一个空目录
mkdir("/home/xxx/a", 0755);
// 将空目录删除(以下两条语句等价)
rmdir("/home/xxx/a");
remove("/home/xxx/a");
}