本章将会讨论如下主题内容。
⚫ Linux 系统的文件类型;
⚫ stat 系统调用;
⚫ 文件各种属性介绍:文件属主、访问权限、时间戳;
⚫ 符号链接与硬链接;
⚫ 目录;
⚫ 删除文件与文件重命名。
在 Linux 系统下,可以通过 stat 命令或者 ls 命令来查看文件类型,如下所示:
其中第一个字符(’ - ‘)就用于表示文件的类型,减号’ - '就表示该文件是一个普通文件看,看其它文件类型使用什么字符表示:
⚫ ’ - ':普通文件
⚫ ’ d ':目录文件
⚫ ’ c ':字符设备文件
⚫ ’ b ':块设备文件
⚫ ’ l ':符号链接文件
⚫ ’ s ':套接字文件
⚫ ’ p ':管道文件
Linux 下可以使用 stat 命令查看文件的属性,其实这个命令内部就是通过调用 stat()函数来获取文件属性的,stat 函数是 Linux 中的系统调用。
#include
#include
#include
int stat(const char *pathname, struct stat *buf);
pathname:用于指定一个需要查看属性的文件路径。
buf:struct stat 类型指针,用于指向一个 struct stat 结构体变量。调用 stat 函数的时候需要传入一个 struct stat 变量的指针,获取到的文件属性信息就记录在struct stat
结构体中。
返回值:成功返回 0;失败返回-1,并设置 error。
1、struct stat 结构体
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; /* 文件状态最后被改变的时间 */
};
1.1、st_mode 变量
st_mode 是 structstat 结构体中的一个成员变量,是一个 32 位无符号整形数据,该变量记录了文件的类型、文件的权限这些信息,其表示方法如下所示:
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 是 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(管道文件)
我们还可以使用 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)) {
/* 是 */
}
2、struct timespec 结构体
struct timespec
{
time_t tv_sec; /* 秒 */
syscall_slong_t tv_nsec; /* 纳秒 */
};
编程实战练习 1
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
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);
}
#include
#include
#include
#include
#include
#include
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);
}
1、fstat 函数
fstat 与 stat 区别在于,stat 是从文件名出发得到文件属性信息,不需要先打开文件;而 fstat 函数则是从文件描述符出发得到文件属性信息,所以使用 fstat 函数之前需要先打开文件得到文件描述符。
2、lstat 函数
lstat()与 stat、fstat 的区别在于,对于符号链接文件,stat、fstat 查阅的是符号链接文件所指向的文件对应的文件属性信息,而 lstat 查阅的是符号链接文件本身的属性信息。
#include
#include
#include
int lstat(const char *pathname, struct stat *buf);
Linux 是一个多用户操作系统,系统中一般存在着好几个不同的用户,而 Linux 系统中的每一个文件都有一个与之相关联的用户和用户组,通过这个信息可以判断文件的所有者和所属组。
文件所有者表示该文件属于“谁”,也就是属于哪个用户。一般来说文件在创建时,其所有者就是创建该文件的那个用户。譬如,当前登录用户为 dt,使用 touch 命令创建了一个文件,那么这个文件的所有者就是 dt;同理,在程序中调用 open 函数创建新文件时也是如此,执行该程序的用户是谁,其文件所有者便是谁。
Linux系统通过用户 ID(UID)或组 ID(GID)就可以识别出不同的用户和用户组。
既然 Linux 下的每一个文件都有与之相关联的用户 ID 和组 ID,那么对于一个进程来说亦是如此,与一个进程相关联的 ID 有 5 个或更多,如下表所示:
⚫ 实际用户 ID 和实际组 ID 标识我们究竟是谁,也就是执行该进程的用户是谁、以及该用户对应的所属组;实际用户 ID 和实际组 ID 确定了进程所属的用户和组。
⚫ 进程的有效用户 ID、有效组 ID 以及附属组 ID 用于文件访问权限检查。
1、有效用户 ID 和有效组 ID
首先对于有效用户 ID 和有效组 ID 来说,这是进程所持有的概念,对于文件来说,并无此属性!有效用户 ID 和有效组 ID 是站在操作系统的角度,用于给操作系统判断当前执行该进程的用户在当前环境下对某个文件是否拥有相应的权限。
在Linux系统中,当进行权限检查时,并不是通过进程的实际用户和实际组来参与权限检查的,而是通过有效用户和有效组来参与文件权限检查。通常,绝大部分情况下,进程的有效用户等于实际用户(有效用户 ID 等于实际用户 ID),有效组等于实际组(有效组 ID 等于实际组 ID)。
2、chown 函数
chown 是一个系统调用,该系统调用可用于改变文件的所有者(用户 ID)和所属组(组 ID)。其实在Linux 系统下也有一个 chown 命令,该命令的作用也是用于改变文件的所有者和所属组,譬如将 testApp.c文件的所有者和所属组修改为 root:
sudo chown root:root testApp.c
#include
int chown(const char *pathname, uid_t owner, gid_t group);
函数参数和返回值如下所示:
pathname:用于指定一个需要修改所有者和所属组的文件路径。
owner:将文件的所有者修改为该参数指定的用户(以用户 ID 的形式描述);
group:将文件的所属组修改为该参数指定的用户组(以用户组 ID 的形式描述);
返回值:成功返回 0;失败将返回-1,兵并且会设置 errno。
测试
#include
#include
#include
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,这两个系统调用函数原型如下所示:
#include
#include
uid_t getuid(void);
gid_t getgid(void);
#include
#include
#include
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 函数就不会报错。
3、fchown 和 lchown 函数
fchown()、lchown()这两个函数与 chown()的区别就像是 fstat()、lstat()与 stat 的区别。
struct stat 结构体中的 st_mode 字段记录了文件的访问权限位。当提及到文件时,指的是前面给大家介绍的任何类型的文件,并不仅仅指的是普通文件;所有文件类型(目录、设备文件)都有访问权限(accesspermission),可能有很多人认为只有普通文件才有访问权限,这是一种误解!
普通权限
譬如使用 ls 命令或 stat 命令可以查看到文件的这 9 个访问权限,如下所示:
而对于进程来说,参与文件权限检查的是进程的有效用户、有效用户组以及进程的附属组用户。
如何判断权限,首先要搞清楚该进程对于需要进行操作的文件来说是属于哪一类“角色”:
⚫ 如果进程的有效用户 ID 等于文件所有者 ID(st_uid),意味着该进程以文件所有者的角色存在;
⚫ 如果进程的有效用户 ID 并不等于文件所有者 ID,意味着该进程并不是文件所有者身份;但是进程的有效用户组 ID 或进程的附属组 ID 之一等于文件的组 ID(st_gid),那么意味着该进程以文件所属组成员的角色存在,也就是文件所属组的同组用户成员。
⚫ 如果进程的有效用户 ID 不等于文件所有者 ID、并且进程的有效用户组 ID 或进程的所有附属组 ID均不等于文件的组 ID(st_gid),那么意味着该进程以其它用户的角色存在。
⚫ 如果进程的有效用户 ID 等于 0(root 用户),则无需进行权限检查,直接对该文件拥有最高权限。
特殊权限
从高位到低位依次表示文件的 set-user-ID 位权限、set-group-ID 位权限以及 sticky 位权限,如下所示:
1、检查文件权限 access
#include
int access(const char *pathname, int mode);
函数参数和返回值含义如下:
pathname:需要进行权限检查的文件路径。
mode:该参数可以取以下值:
⚫ F_OK:检查文件是否存在
⚫ R_OK:检查是否拥有读权限
⚫ W_OK:检查是否拥有写权限
⚫ X_OK:检查是否拥有执行权限
除了可以单独使用之外,还可以通过按位或运算符" | "组合在一起。
返回值:检查项通过则返回 0,表示拥有相应的权限并且文件存在;否则返回-1,如果多个检查项组合在一起,只要其中任何一项不通过都会返回-1。
2、修改文件权限 chmod
#include
int chmod(const char *pathname, mode_t mode);
函数参数及返回值如下所示:
pathname:需要进行权限修改的文件路径,若该参数所指为符号链接,实际改变权限的文件是符号链接所指向的文件,而不是符号链接文件本身。
mode:该参数用于描述文件权限,与 open 函数的第三个参数一样,这里不再重述,可以直接使用八进制数据来描述,也可以使用相应的权限宏(单个或通过位或运算符" | "组合)。
返回值:成功返回 0;失败返回-1,并设置 errno。
文件权限对于文件来说是非常重要的属性,是不能随随便便被任何用户所修改的,要想更改文件权限,要么是超级用户(root)进程、要么进程有效用户 ID 与文件的用户 ID(文件所有者)相匹配。
fchmod 函数
该函数功能与 chmod 一样,参数略有不同。
在 Linux 系统中有两种链接文件,分为软链接(也叫符号链接)文件和硬链接文件,软链接文件也就是前面给大家的 Linux 系统下的七种文件类型之一,其作用类似于 Windows 下的快捷方式。
从使用角度来讲,两者没有任何区别,都与正常的文件访问方式一样,支持读、写以及执行。
使用 ln 命令可以为一个文件创建软链接文件或硬链接文件,用法如下:
硬链接:ln 源文件 链接文件
软链接:ln -s 源文件 链接文件
从上图中可知,使用 ln 命令创建的两个硬链接文件与源文件 test_file 都拥有相同的 inode 号,既然inode 相同,也就意味着它们指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已,创建出来的硬链接文件与源文件对文件系统来说是完全平等的关系。每删除一个硬链接,inode 节点上的链接数就会减一,直到为 0,inode 节点和对应的数据块才会被文件系统所回收,也就意味着文件已经从文件系统中被删除了。
软链接文件与源文件有着不同的 inode 号,如下图所示,所以也就是意味着它们之间有着不同的数据块,但是软链接文件的数据块中存储的是源文件的路径名,链接文件可以通过这个路径找到被链接的源文件,它们之间类似于一种“主从”关系,当源文件被删除之后,软链接文件依然存在,但此时它指向的是一个无效的文件路径,这种链接文件被称为悬空链接。
介绍完它们之间的区别之后,大家可能觉得硬链接相对于软链接来说有较大的优势,其实并不是这样,对于硬链接来说,存在一些限制情况,如下:
⚫ 不能对目录创建硬链接(超级用户可以创建,但必须在底层文件系统支持的情况下)。
⚫ 硬链接通常要求链接文件和源文件位于同一文件系统中。
而软链接文件的使用并没有上述限制条件,优点如下所示:
⚫ 可以对目录创建软链接;
⚫ 可以跨越不同文件系统;
⚫ 可以对不存在的文件创建软链接。
1、创建链接文件
创建硬链接 link()
link()系统调用用于创建硬链接文件,函数原型如下(可通过"man 2 link"命令查看):
#include
int link(const char *oldpath, const char *newpath);
oldpath:用于指定被链接的源文件路径,应避免 oldpath 参数指定为软链接文件,为软链接文件创建硬链接没有意义,虽然并不会报错。
newpath:用于指定硬链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误。返回值:成功返回 0;失败将返回-1,并且会设置 errno。
link 函数测试
#include
#include
#include
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()
#include
int symlink(const char *target, const char *linkpath);
函数参数和返回值含义如下:
target:用于指定被链接的源文件路径,target 参数指定的也可以是一个软链接文件。
linkpath:用于指定硬链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误。
返回值:成功返回 0;失败将返回-1,并会设置 errno。
创建软链接时,并不要求 target 参数指定的文件路径已经存在,如果文件不存在,那么创建的软链接将成为“悬空链接”。
symlink 函数测试
#include
#include
#include
int main(void)
{
int ret;
ret = symlink("./test_file", "./soft");
if (-1 == ret)
{
perror("symlink error");
exit(-1);
}
exit(0);
}
2、读取软链接文件
#include
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
函数参数和返回值含义如下:
pathname:需要读取的软链接文件路径。只能是软链接文件路径,不能是其它类型文件,否则调用函数将报错。
buf:用于存放路径信息的缓冲区。
bufsiz:读取大小,一般读取的大小需要大于链接文件数据块中存储的文件路径信息字节大小。
返回值:失败将返回-1,并会设置 errno;成功将返回读取到的字节数。
readlink 函数测试
#include
#include
#include
#include
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,并将读取到的信息打印出来,测试如下:
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]; /* 文件名 */
};
#include
#include
#include
#include
#include
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);
}
1、进程的当前工作目录
Linux 下的每一个进程都有自己的当前工作目录(current working directory),当前工作目录是该进程解析、搜索相对路径名的起点(不是以" / "斜杆开头的绝对路径)。譬如,代码中调用 open 函数打开文件时,传入的文件路径使用相对路径方式进行表示,那么该进程解析这个相对路径名时、会以进程的当前工作目录作为参考目录。
#include
#include
#include
#include
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);
}
改变当前工作目录
#include
#include
#include
#include
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);
}
2、删除文件
使用 unlink 函数删除文件
#include
int unlink(const char *pathname);
函数参数和返回值含义如下:
pathname:需要删除的文件路径,可使用相对路径、也可使用绝对路径,如果 pathname 参数指定的文件不存在,则调用 unlink()失败。
返回值:成功返回 0;失败将返回-1,并设置 errno。
#include
#include
#include
int main(void)
{
int ret;
ret = unlink("./test_file");
if (-1 == ret) {
perror("unlink error");
exit(-1);
}
exit(0);
}
#include
int remove(const char *pathname);
#include
#include
int main(void)
{
int ret;
ret = remove("./test_file");
if (-1 == ret)
{
perror("remove error");
exit(-1);
}
exit(0);
}
#include
int rename(const char *oldpath, const char *newpath);
函数参数和返回值含义如下:
oldpath:原文件路径。
newpath:新文件路径。
返回值:成功返回 0;失败将返回-1,并设置 errno。
根据 oldpath、newpath 的不同,有以下不同的情况需要进行说明:
⚫ 若 newpath 参数指定的文件或目录已经存在,则将其覆盖;
⚫ 若 newpath 和 oldpath 指向同一个文件,则不发生变化(且调用成功)。
⚫ rename()系统调用对其两个参数中的软链接均不进行解引用。如果 oldpath 是一个软链接,那么将重命名该软链接;如果 newpath 是一个软链接,则会将其移除、被覆盖。
⚫ 如果 oldpath 指代文件,而非目录,那么就不能将 newpath 指定为一个目录的路径名。要想重命名一个文件到某一个目录下,newpath 必须包含新的文件名。
⚫ 如果 oldpath 指代为一个目录,在这种情况下,newpath 要么不存在,要么必须指定为一个空目录。
⚫ oldpath 和 newpath 所指代的文件必须位于同一文件系统。由前面的介绍,可以得出此结论!
⚫ 不能对.(当前目录)和…(上一级目录)进行重命名。
#include
#include
int main(void)
{
int ret;
ret = rename("./test_file", "./new_file");
if (-1 == ret)
{
perror("rename error");
exit(-1);
}
exit(0);
}
开头先给大家介绍 Linux 系统下的 7 种文件类型,包括普通文件、目录、设备文件(字符设备文件、块设备文件)、符号链接文件(软链接文件)、管道文件以及套接字文件。
接着围绕 stat 系统调用,详细给大家介绍了 struct stat 结构体中的每一个成员,这使得我们对 Linux 下文件的各个属性都有所了解。接着分别给大家详细介绍了文件属主、文件访问权限、文件时间戳、软链接与硬链接以及目录等相关内容,让大家知道在应用编程中如何去修改文件的这些属性以及它们所需要满足的条件。