《UNIX 环境高级编程》学习笔记——文件和目录

2022/1/13 20:06:40

本文主要是介绍《UNIX 环境高级编程》学习笔记——文件和目录,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

UNIX环境高级编程——文件和目录

  • 函数 stat、fstat、fstatat 和 lstat
  • 文件类型
  • 设置用户ID和设置组ID
  • 文件访问权限
  • 新文件和目录的所有权
  • 函数 access 和 faccessat
  • 函数 umask
  • 函数 chmod、fchmod 和 fchmodat
  • 粘着位
  • 函数 chown、fchown、fchownat 和 lchown
  • 文件长度
  • 文件系统
  • 函数 link、linkat、unlink、unlinkat 和 remove
  • 函数 rename 和 renameat
  • 符号链接
  • 创建和读取符号链接
  • 文件的时间
  • 函数 utimes
  • 函数 mkdir、mkdirat 和 rmdir
  • 读目录
  • 函数 chdir、fchdir 和 getcwd
  • 设备特殊文件
  • 文件访问权限小结

函数 stat、fstat、fstatat 和 lstat

int stat(const char* restrict pathname, struct stat* restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char* restrict pathname, struct stat* restrict buf);
int fstatat(int fd, const char *resrict pathname, struct stat *restrict buf, int flag);
                                         所有4个函数的返回值:若成功,返回0;若出错,返回-1
struct stat
{
	mode_t st_mode;
	ino_t st_ino;
	dev_t st_dev;
	dev_t st_rdev;
	nlink_t st_nlink;
	uid_t st_uid;
	gid_t st_gid;
	off_t st_size;
	struct timespec st_atime;
	struct timespec st_mtime;
	struct timespec st_ctime;
	blksize_t st_blksize;
	blkcnt_t st_blocks;
};

两者的差别在于,文件指向一个符号链接时,stat返回符号链接所链接文件信息,lstat返回文件自身信息。

文件类型

文件类型包括如下几种。

  • 普通文件
    包含了某种形式的数据。至于数据是文本还是二进制,对UNIX内核而言无区别。
    对普通文件内容的解释由处理该文件的应用程序进行。

  • 目录文件
    包含了其他文件的名字,及指向与这些文件有关信息的指针。
    只有内核可以直接写目录文件。

  • 块特殊文件
    提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。

  • 字符特殊文件
    提供对设备不带缓冲的访问,每次访问长度可变。
    系统中的所有设备要么是字符特殊文件,要么是块特殊文件。

  • FIFO
    用于进程间通信,有时也称为命名管道。

  • 套接字
    用于进程间的网络通信。
    套接字也可用于在一台宿主机上进程之间的非网络通信。

  • 符号链接
    指向另一个文件。

文件类型信息包含在 stat 结构的 st_mode 成员中。
这些宏的参数都是 stat 结构中的 st_mode 成员。

文件类型
S_ISREG()普通文件
S_ISDIR()目录
S_ISCHR()字符特殊文件
S_ISBLK()块特殊文件
S_ISFIFO()管道或FIFO
S_ISSOCK()套接字
S_ISLINK()符号链接

POSIX.1允许实现将进程间通信(IPC)对象(如消息队列和信号量等)说明为文件。
下面的宏可用来从 stat 结构中确定 IPC 对象的类型。

对象的类型
S_TYPEISMQ()消息队列
S_TYPEISSEM()信号量
S_TYPEISSHM()共享存储对象

对文件可读/可写/可执行理解

  • 对于普通文件而言:
    1.读权限允许用户标识读取该文件;
    2.写权限允许用户修改该文件;
    3.执行权限允许用户标识执行该文件:
    a. 对于一个不可执行的文件来说,拥有执行权限是没有任何意义的;
    b. 如果文件是一个程序或者某种类型的脚本时,那么它就是可执行的 。

  • 对于目录而言:
    1.读权限允许用户标识读取目录中的文件名,只能列举目录中的文件名,不能进入该目录,相应也不能查看目录下各文件的大小;
    2.写权限允许用户标识修改目录(创建、移动、复制、删除);
    3.执行权限允许用户搜索该目录:

设置用户ID和设置组ID

- 对进程

在这里插入图片描述

  • 实际用户ID、实际组ID取决于当前登录系统的用户。
  • 有效用户ID、有效组ID是文件访问权限检查的依据。
    默认下,采用实际用户ID、实际组ID的值。
  • 保存的设置用户ID和保持的设置组ID。
    用于记录有效用户ID、有效组ID值。

每个文件有一个所有者和组所有者,所有者由stat结构中的 st_uid指定,组所有者则由 st_gid 指定。

当执行一个程序文件时,进程的有效用户ID通常就是实际用户ID,有效组ID通常是实际组ID。

可在文件模式字(st_mode)中设置一个特殊标志,其含义是“当执行此文件时,经进程的有效用户ID设置为文件所有者的用户ID(st_uid)”。
在文件模式字中可以设置另一位,它将执行此文件的进程的有效组ID设置为文件的组所有者ID(st_gid)。

在文件模式字中的这两位被称为设置用户ID(set-user-ID )位和设置组ID(set-group-ID)位。

文件访问权限

st_mode 值也包含了对文件的访问权限位。
所有文件类型(目录、字符特别文件等)都有访问权限。

每个文件有9个访问权限位,可将它们分成3类。
在这里插入图片描述

不同的使用方式汇总:

  • 第一个规则是,我们用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。
  • 对于一个文件的读权限决定了我们是否能够打开现有文件进行读操作。这与open函数的 O_RDONLY 和 O_RDWR标志相关。
  • 对于一个文件的写权限决定了我们是否能够打开现有文件进行写操作。这与open函数的 O_WRONLY 和 O_RDWR标志相关。
  • 为了在 open 函数中对一个文件指定 O_TRUNC 标志,必须对该文件具有写权限。
  • 为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。
  • 为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限。对该文件本身则不需要有读、写权限。
  • 如果用7个 exec 函数中的任何一个指向某个文件,都必须对该文件具有执行权限。该文件还必须是一个普通文件。

内核进行的测试具体如下。

  • (1)若进程的有效用户 ID 是0(超级用户),则允许访问。这给予了超级用户对整个文件系统进行处理的最充分的自由。
  • (2)若进程的有效用户 ID 等于文件的所有者 ID(也就是进程拥有此文件),那么如果所有者适当的访问权限位被设置,则允许访问;否则拒绝访问。
    适当的访问权限位指的是,若进程为读而打开该文件,则用户读位应为1;若进程为写而打开该文件,则用户写位应为1;若进程将执行该文件,则用户执行位应为1.
  • (3)若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果组适当的访问权限位被设置,则允许访问;否则拒绝访问。
  • (4)若其他用户适当的访问权限位被设置,则允许访问;否则拒绝访问。

新文件和目录的所有权

新文件的用户 ID 设置为进程的有效用户ID。

关于组ID,POSIX.1允许实现选择下列之一作为新文件的组ID。

  • (1)新文件的组ID可以是进程的有效组ID。
  • (2)新文件的组ID可以是它所在目录的组ID。

函数 access 和 faccessat

依据进程的实际ID/实际组ID执行对文件执行权限测试

#include <unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
                                     两个函数的返回值:若成功,返回0;若出错,返回-1

其中,如果测试文件是否已经存在,mode 就为 F_OK;否则,如下图:
在这里插入图片描述

函数 umask

// 进程用open创建文件时,文件的模式为 参数mode & ~(cmask)

#include <sys/stat.h>
mode_t umask(mode_t cmask);
                            返回值:之前的文件模式创建屏蔽字

在进程创建一个新文件或新目录时,就一定会使用文件模式创建屏蔽字。

在文件模式创建屏蔽字中为1的位,在文件 mode 中的相应位一定被关闭。

函数 chmod、fchmod 和 fchmodat

进程更改现有文件的访问权限。
进程为超级进程或进程的有效ID等于文件的所有者ID,进程才有权限对文件执行此函数。

#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
                                             3个函数返回值:若成功,返回0;若出错,返回-1
  • chmod 函数在指定的文件上进行操作。
  • fchmod 函数则对已打开的文件进行操作。
  • fchmodat 函数与 chmod 函数在下面两种情况下是相同的:一种是 pathname 参数为绝对路径,另一种是 fd 参数取值为 AT_FDCWD 而 pathname 参数为相对路径。
    否则,fchmodat 计算相对于打开目录(由 fd 参数指向)的 pathname 。
  • flag 参数可以用于改变 fchmodat 的行为,当设置了 AT_SYMLINK_NOFOLLOW 标志,fchmodat 并不会跟随符号链接。

在这里插入图片描述

chmod 函数在下列条件下自动清除两个权限位。

  • Solaris 等系统对用于普通文件的粘着位赋予了特殊含义,在这些系统上如果我们试图设置普通文件的粘着位(S_ISVTX),而且又没有超级用户权限,那么 mode 中的粘着位自动被关闭。
  • 新创建文件的组ID可能不是调用进程所属的组。

粘着位

如果对一个目录设置了粘着位,只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录下的文件:

  • 拥有此文件;
  • 拥有此目录;
  • 是超级用户。

目录 /tmp 和 /var/tmp 是设置粘着位的典型候选者——任何用户都可以在这两个目录中创建文件。
任一用户(用户、组合其他)对这两个目录的权限通常都是读、写和执行。

函数 chown、fchown、fchownat 和 lchown

更改已经存在文件的ID和组ID。

#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag);
int lchown(const char *pathname, uid_t owner, gid_t group);
                                     4个函数的返回值:若成功,返回0;若出错,返回-1
  • fchown 函数改变 fd 参数指向的打开文件的所有者,既然它在一个已打开的文件上操作,就不能用于改变符号链接的所有者。

  • fchownat 函数与 chown 或者 lchown 函数在下面两种情况下是相同的:一种是 pathname 参数为绝对路径,另一种是 fd 参数取值为 AT_FDCWD 而 pathname 参数为相对路径。

文件长度

stat 结构成员 st_size表示以字节为单位的文件长度。
此字段只对普通文件,目录文件,符号链接有意义。

大多数现代的UNIX系统提供字段 st_blksize 和
st_blocks。

文件中的空洞

空洞是由所设置的偏移量超过文件尾端,并写入了某些数据后造成的。

文件截断

修改一个已经存在文件可访问长度。

#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
                              两个函数的返回值:若成功,返回0;若出错,返回-1

这两个函数将一个现有文件长度截断为 length 。
如果该文件以前的长度大于 length,则超过 length 以外的数据就不能再访问。
如果以前的长度小于 length ,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0(也就可能在文件中创建了一个空洞)。

文件系统

在这里插入图片描述

  • 在图中有两个目录项指向同一个 i 节点。每个i节点有一个链接计数(指向该i节点的目录项数)。
    链接计数为0时,可释放此i节点所指向的数据块集合。
    i节点的链接计数,通过stat.st_nlink可在代码中获取
    图中的链接为硬链接。

在这里插入图片描述

  • 一种叫做符号链接的文件,在该文件i节点指向数据块中存储了一个文件路径(为其所链接的文件)。
    对应文件类型常量为S_IFLNK。

  • i节点含有文件有关的所有信息:文件类型、访问权限、文件长度和指向文件数据块的指针等。

  • 目录块每一项,i节点编号(必须为同一文件系统下的)。

  • 当在不更换文件系统的情况下为一个文件重命名时,该文件的实际内容并未移动,只需构造一个指向现有 i 节点的新目录项,并删除老的目录项。链接计数不会改变。

函数 link、linkat、unlink、unlinkat 和 remove

创建一个指向现有文件的链接的方法是使用 link 函数或 linkat 函数。

#include <hnistd.h>
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
                                  两个函数的返回值:若成功,返回0;若出错,返回-1

这两个函数创建一个新目录项 newpath,它引用现有文件 existingpath 。如果newpath 已经存在,则返回出错。只创建 newpath 中的最后一个分量,路径中的其他部分应当已经存在。

对于 linkat 函数,现有文件是通过 efd 和 existingpath 参数指定的,新的路径名是通过 nfd 和 newpath 参数指定的。

为了删除一个现有的目录项,可以调用 unlink 函数。

#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd, const char *pathname, int flag);
                                      两个函数的返回值:若成功,返回0;若出错,返回-1

这两个函数删除目录项,并将由 pathname 所引用文件的链接计数减1。
如果该文件还有其他链接,则让可通过其他链接访问该文件的数据。如果出错,则不对该文件做任何更改。

即使一个i节点的引用计数变为0,但如果此i节点此时被文件表项所指向(也即处于打开状态),此i节点也不会被删除。
当此i节点无文件表现指向,引用计数又是0时,系统会释放此i节点。

函数 rename 和 renameat

#include <stdio.h>
int rename(const char *oldname, const char *newname);
int renameat(int oldfd, const char *oldname, int newfd, const char *newname);
                             两个函数的返回值:若成功,返回0;若出错,返回-1

符号链接

用link创建的是硬连接,底层文件仍然只有一个i节点。

符号链接的产生一个独立的文件,此文件的内容是其所链接文件的路径名。

一般的文件I/O函数,传入符号链接文件时,会自动替换为其所链接文件的路径再进行后续处理。

创建和读取符号链接

#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
                            两个函数的返回值:若成功,返回0;若出错,返回-1

函数创建了一个指向 actualpath 的新目录项 sympath 。
在创建此符号链接时,并不要求 actualpath 已经存在。并且, actualpath 和 sympath 并不需要位于同一文件系统中。

读出符号链接文件自身内容,并读出该链接的名字。

#include <unistd.h>
ssize_t readlink(const char *restrict pathname, char *restrict buf, size_t bufsize);
ssize_t readlinkat(int fd, const char *restrict pathname, char *restrict buf, 
					size_t bufsize, size_t bufsize);
					               两个函数的返回值:若成功,返回读取的字节数;若出错,返回-1

文件的时间

在这里插入图片描述

函数 utimes

人为设置文件的最后访问,修改时间。

#include <sys/time.h>
int utimes(const char *pathname, const struct timeval times[2]);	
                                        函数返回值:若成功,返回0;若出错,返回-1

utimes 函数对路径名进行操作。
times 参数是指向包含两个时间戳(访问时间和修改时间)元素的数组的指针,两个时间戳是用秒和微秒表示的。

struct timeval
{
	time_t  tv_sec;
	long tv_usec;
};

函数 mkdir、mkdirat 和 rmdir

#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
                                 两个函数返回值:若成功,返回0;若出错,返回-1

这两个函数创建一个新的空目录。
其中, . 和 … 目录项是自动创建的。
所指定的文件访问权限 mode 由进程的文件模式创建屏蔽字修改。

常见的错误是指定与文件相同的 mode (只指定读、写权限)。但是,对于目录通常至少要设置一个指向权限位,以运行访问该目录中的文件名。

用 rmdir 函数可以删除一个空目录。
空目录是只包含 . 和 … 这两项的目录。

#include <unistd.h>
int rmdir(const char* pathname);
                                 返回值:若成功,返回0;若出错,返回-1

读目录

对某个目录具有访问权限的任一用户都可以读该目录,但是,为了放在文件系统产生混乱,只有内核才能写目录。

目录文件结构依赖特定实现,需借助系统API由系统与其交互。

#include <dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
                          两个函数返回值:若成功,返回指针;若出错,返回 NULL

struct dirent* readdir(DIR* dp);
                         返回值:若成功,返回指针;若在目录尾或出错,返回 NULL

int closedir(DIR* dp);
                         返回值:若成功,返回0;若出错,返回-1

函数 chdir、fchdir 和 getcwd

设置和获取进程当前目录

#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
                      两个函数的返回值:若成功,返回0;若出错,返回-1

在这两个函数中,分别用 pathname 或打开文件描述符来指定新的当前工作目录。

函数从当前工作目录(.)开始,用 … 找到其上一级目录,然后读其目录项,直到该目录项中的 i 节点编号与工作目录 i 节点编号相同,这样地就找到了其对应的文件名。

#include <unistd.h>
char *getcwd(char *buf, size_t size);
                            返回值:若成功,返回buf;若出错,返回 NULL

必须向此函数传递两个参数,一个是缓冲区地址 buf ,另一个是缓冲区的长度 size (以字节为单位)。
该缓冲区必须由足够的长度以容纳绝对路径名再加上一个终止 null 字节,否则返回出错。

设备特殊文件

st_dev 和 st_rdev 这两个字段经常引起混淆。
编写 ttyname 函数时,需要使用这两个字段。

有关规则很简单:

  • 每个文件系统所在的存储设备都由其主、次设备号表示。
  • 通常可以使用两个宏:major 和 minor 来访问主、次设备号,大多数实现都定义这两个宏。
  • 系统中与每个文件名关联的 st_dev 值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的 i 节点。
  • 只有字符特殊文件和块特殊文件才有 st_rdev 值。此值包含实际设备的设备号。

文件访问权限小结

在这里插入图片描述

学习参考资料:

《UNIX 环境高级编程》第3版


这篇关于《UNIX 环境高级编程》学习笔记——文件和目录的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程