4 文件和目录
一、 stat 结构和权限相关
四个
stat
函数:返回文件或者目录的信息结构:#include<sys/stat.h> 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*restrict pathname,struct stat*restrict buf,int flag);
参数:
pathname
:文件或者目录的名字buf
:存放信息结构的缓冲区fd
:打开的文件描述符- 对于
fstat
,该文件就是待查看信息的文件 - 对于
fstatat
,该文件是并不是待查看信息的文件。待查看信息的文件时已该fd
对于的目录相对路径定位的
- 对于
flag
:控制着fstatat
函数是否跟随一个符号链接。对于
fstatat
函数:待查看的文件名是由
fd
和pathname
共同决定的。- 如果
pathname
是个绝对路径,则忽略fd
参数 - 如果
pathname
是个相对路径路径,且fd=AT_FDCWD
,则在当前工作目录的路径下查找pathname
- 如果
pathname
是个相对路径路径,且fd!=AT_FDCWD
,则在fd
对应的打开目录下查找pathname
- 如果
flag
:控制着fstatat
函数是否跟随一个符号链接。当!AT_SYMLINK_FOLLOW
标志被设置时,查看的是pathname
(如果它是个符号链接)本身的信息;否则默认查看的是pathname
(如果它是个符号链接)链接引用的文件的信息。
返回值:
- 成功:返回 0
- 失败: 返回 -1
注意:
lstat
类似于stat
,但是当pathname
是个符号链接时,lstat
查看的是该符号链接的有关信息;而stat
是查看该符号链接引用的文件的信息。- 在
ubuntu 16.04
上,虽然有AT_SYMLINK_NOFOLLOW
这个常量,但是不支持。必须用!AT_SYMLINK_FOLLOW
。其常量定义为:AT_SYMLINK_FOLLOW
: 1024 (有效)!AT_SYMLINK_FOLLOW
: 0(有效)AT_SYMLINK_NOFOLLOW
: 256(无效)AT_SYMLINK_FOLLOW
: -1025(无效)
stat
数据结构:其定义可能与具体操作系统相关,但是基本形式为:struct stat{ mode_t st_mode; //文件权限和类型信息 ino_t st_ino; //i-node 号 dev_t st_dev; // 设备号 dev_t st_rdev; // 特殊文件的设备号 nlink_t st_nlink; // 硬链接数量 uid_t st_uid; // owner 的用户 ID gid_t st_gid; // owner 的组 ID off_t st_size; //对普通文件,它是文件字节大小 struct timespec st_atime; // 上次访问时间 struct timespec st_mtile; // 上次修改时间 struct timespec st_ctime; // 上次文件状态改变的时间 blksize_t st_blksize; // 最佳的 I/O block 大小 blkcnt_t st_blocks; //分配的磁盘块数量 }
其中
timespec
结构与具体操作系统相关,但是至少包括下面两个字段:struct timespec{ time_t tv_sec; // 秒 long tv_nsec; //纳秒 }
UNIX 文件类型:
- 普通文件:最常见的文件类型,这种文件包含了某种形式的数据。至于这种数据是二进制还是文本,对内核无区别。普通文件的内容解释由具体的应用程序进行。
- 目录文件:这种文件包含了其他文件的名字,以及指向这些文件有关信息的指针。
- 只有内核可以直接写目录文件(通常用户写目录文件都要通过内核)
- 对某个目录文件具有读权限的任何一个进程都可以读取该目录的内容
- 块特殊文件:这种类型的文件提供对设备(如磁盘)带缓冲的访问。每次访问以固定长度为单位进行。
- 字符特殊文件:这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变。
系统的所有设备,要么是字符特殊文件,要么是块特殊文件
FIFO
:这种类型的文件用于进程间通信,有时也称为命名管道- 套接字:这种类型的文件用于进程间的网络通信(也可用于单机上进程的非网络通信)
符号链接:这种类型的文件指向另一个文件
文件类型信息存放在
stat.st_mode
成员中,可以用下列的宏测试文件类型:S_ISREG()
:测试是否普通文件S_ISDIR()
:测试是否目录文件S_ISCHR()
:测试是否字符特殊文件S_ISBLK()
:测试是否块特殊文件S_ISFIFO()
:测试是否FIFO
S_ISLNK()
:测试是否符号链接文件S_ISSOCK()
:测试是否套接字另外
POSIX.1
允许将进程间通信对象说明为文件。但是下面的宏测试的不是stat.st_mode
,而是stat*
(stat
指针):S_TYPEISMQ()
:测试是否消息队列S_TYPEISSEM()
:测试是否信号量S_TYPEISSHM()
:测试是否共享存储对象
与一个进程有关的 ID 有很多:
- 实际用户 ID 和实际组 ID: 标志我们究竟是谁。当我们登录进操作系统时,这两个值就确定了!
- 有效用户 ID、有效组 ID、附属组 ID: 用于文件访问权限检查。
保存的设置用户 ID、保存的设置组 ID:由
exec
函数保存每个文件都有一个所有者和组所有者,分别有
stat.st_uid
和stat.st_gid
指定。当一个文件时可执行文件时,如果执行这个文件,那么进程的有效用户 ID 就是实际用户 ID,有效组 ID 就是实际组 ID,除了下面的情况:- 当在
stat.st_mode
中设置了一个特殊标志:设置用户 ID 位时,则将进程的有效用户 ID 设置为文件所有者的用户 ID 当在
stat.st_mode
中设置了一个特殊标志:设置组 ID 位时,则将进程的有效组 ID 设置为文件所有者的组 ID任何进程都是由可执行文件被执行而得到。因此位于磁盘上的可执行文件的所属的用户 ID 和组 ID 会影响到进程的用户 ID 和组 ID
如果某个可执行文件所有者是
root
,且该文件的设置用户 ID 位已经被设置,那么无论谁执行这个可执行文件时,该可执行文件产生的进程就具有超级用户权限。设置用户 ID 位、设置组 ID 位 都包含在
stat.st_mode
中,可以通过下列两个宏测试:S_ISUID()
:测试是否设置了设置用户 ID 位S_ISGID()
:测试是否设置了设置组 ID 位
文件访问权限:所有文件类型(包括目录,字符特别文件等)都有访问权限。每个文件都有 9 个访问权限位:
S_IRUSR
:用户读S_IWUSR
:用户写S_IXUSR
:用户执行S_IRGRP
:组读S_IWGRP
:组写S_IXGRP
:组执行S_IROTH
:其他读S_IWOTH
:其他写S_IXOTH
:其他执行访问权限规则:
当用名字
pathname
打开任何一个类型的文件时,对pathname
中包含的每一个目录,包括pathname
可能隐含的当前工作目录都应该具有执行权限因此目录的执行权限位也称之为搜索位
- 对一个文件的读权限决定了我们能否打开现有文件进行读操作
- 对一个文件的写权限决定了我们能否打开现有文件进行写操作
- 如果你在
open
函数中对一个文件指定了O_TRUNC
标志,则必须对该文件具有写权限 - 为了在一个目录中常见一个新文件,必须对该目录具有写权限和执行权限
- 为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限。对该文件本身没有权限的限制
如果用 7 个
exec
函数中的任何一个执行某个文件,则必须对该文件具有执行权限,且该文件必须是个普通文件进程每次打开、创建、删除一个文件时,内核就进行文件访问权限测试。这种测试如下:
若进程的有效用户 ID 是 0(超级用户),则对该文件的任何访问行为都批准
- 若进程的有效用户 ID 等于文件的所有者 ID(也就是进程拥有此文件):
- 如果该文件的用户读权限开放,则内核允许进程读该文件
- 如果该文件的用户写权限开放,则内核允许进程写该文件
- 如果该文件的用户执行权限开放,则内核允许进程执行该文件
- 若进程的有效组 ID 或者进程的附属组 ID 之一等于文件的组 ID:
- 如果该文件的组读权限开放,则内核允许进程读该文件
- 如果该文件的组写权限开放,则内核允许进程写该文件
- 如果该文件的用户执行权限开放,则内核允许进程执行该文件
否则:
- 如果该文件的其他读权限开放,则内核允许进程读该文件
- 如果该文件的其他写权限开放,则内核允许进程写该文件
- 如果该文件的其他户执行权限开放,则内核允许进程执行该文件
只要有一个权限通过,则不再进行测试。若所有权限都不通过,则不允许访问。
对一个目录的读权限和可执行权限是不同的:
- 目录读权限:允许读目录,从而获得在该目录中所有文件名的列表
- 目录可执行权限:允许搜索该目录,从而寻找一个特定的文件名
当一个进程通过
open
或者creat
创建一个新文件时:- 新文件的用户 ID 被设置为进程的有效用户 ID
- 新文件的组 ID 可以有两个值之一:
- 进程的有效组 ID
- 文件所在目录的组 ID
具体选择哪个,由具体操作系统决定
stat
和lstat
示例:在main
函数中调用test_stat_lstat
函数:void test_stat_lstat() { M_TRACE("--------- Begin test_stat_lstat() ---------\n"); Stat stat_buf; My_stat("/home/huaxz1986/APUE/main.c",&stat_buf); // regular file My_stat("/home/huaxz1986/APUE/",&stat_buf); // dir file My_stat("/dev/loop0",&stat_buf); // block file My_stat("/dev/mem",&stat_buf); // char file My_lstat("/dev/cdrom",&stat_buf); // link file My_stat("/run/systemd/initctl/fifo",&stat_buf); // fifo file int fd=My_open_with_mode("test_stat",O_WRONLY|O_CREAT,S_IRUSR); // create a new file close(fd); My_stat("test_stat",&stat_buf); // regular file M_TRACE("--------- End test_stat_lstat() ---------\n\n"); }
二、访问测试和文件模式创建屏蔽字
当用
open()
函数打开一个文件时,内核根据进程的有效用户 ID 和有效组 ID 为依据来执行访问权限测试。但是如果你想测试进程的实际用户 ID 和实际组 ID 是否能够通过权限测试时,可以用下列两个函数:#include<unistd.h> int access(const char *pathname,int mode); int faccess(int fd,const char*pathname,int mode,int flag);
参数:
pathname
:文件路径名mode
:指定要测试的模式。- 如果要测试文件是否已存在,则
mode
设为F_OK
- 如果要测试进程的实际用户 ID 和实际组 ID 的权限,则可以为下列常量的按位或
R_OK
:测试读权限W_OK
:测试写权限X_OK
:测试执行权限
对于
faccess
函数:- 如果要测试文件是否已存在,则
fd
:一个打开目录文件的描述符,或者AT_FDCWD
pathname
:- 如果为绝对路径,则忽略
fd
参数 - 如果为相对路径,则相对路径的目录由
fd
指定。- 若
fd=AT_FDCWD
,则表示相对于当前工作目录 - 否则相对于
fd
对于的打开的目录
- 若
- 如果为绝对路径,则忽略
flag
:如果是AT_EACCESS
,则访问检查使用进程的有效用户 ID 和有效组 ID,而不是实际用户 ID 和实际组 ID
返回值:
- 成功:返回 0
- 出错: 返回 -1
文件模式创建屏蔽字:当进程创建一个新的目录或者文件时,会使用文件模式创建屏蔽字。在文件模式创建屏蔽字中为 1 的位,在文件
mode
中的相应位一定被关闭。设置进程的文件模式创建屏蔽字的函数为:#include<sys/stat.h> mode_t umask(mode_t cmask);
- 参数:
cmask
:要设置的新的文件模式创建屏蔽字
返回值:
- 成功:旧的文件模式创建屏蔽字
- 函数未指定失败时返回何值
如果你在通过
creat
或者open
函数指定了mode
,那么该mode
必须通过文件模式创建屏蔽字的屏蔽之后才是最终新创建的文件的权限模式。umask
指定了哪个,哪个权限就被屏蔽了!shell 有一个
umask
命令。我们可以通过该命令来设置或者打印当前的文件模式创建屏蔽字
- 参数:
示例:测试
umask
和access
函数的用法:在main
函数中调用test_access_umask
函数:void test_access_umask() { My_access("/no/exist",F_OK); // no exist My_access("/etc/shadow",W_OK);// can not write My_access("/home/huaxz1986/APUE",W_OK); // can write print_new_file_mode("test_umask1") ;// old umask //new umask My_umask(S_IRUSR|S_IRGRP|S_IROTH); print_new_file_mode("test_umask2") ;// new umask }
可以看到:
access
函数:对于不存在的文件名访问失败;对没有写权限的名字写访问失败- 被创建的文件的访问权限是由文件创建屏蔽字、创建文件时指定的权限二者共同作用的
三、修改文件访问权限和文件所属用户
修改文件的现有的访问权限:
#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);
参数:
pathname
:文件路径名mode
:文件修改后的权限。对于
fchmod
函数:fd
:打开的文件描述符对于
fchmod
函数:fd
:一个打开目录文件的描述符,或者AT_FDCWD
pathname
:- 如果为绝对路径,则忽略
fd
参数 - 如果为相对路径,则相对路径的目录由
fd
指定。- 若
fd=AT_FDCWD
,则表示相对于当前工作目录 - 否则相对于
fd
对于的打开的目录
- 若
- 如果为绝对路径,则忽略
flag
:如果是!AT_SYMLINK_FOLLOW
,则fchmodtat
并不跟随符号链接
返回值:
- 成功:返回 0
- 出错: 返回 -1
参数
mode
可以是下面常量的按位或:(来自头文件<sys/stat.h>
S_ISUID
:执行时设置用户 IDS_ISGID
:执行时设置组 IDS_ISVTX
:粘着位S_IRWXU
:用户读、写和执行S_IRUSR
:用户读S_IWUSR
:用户写S_IXUSR
:用户执行S_IRWXG
:组读、写和执行S_IRGRP
:用户读S_IWGRP
:用户写S_IXGRP
:用户执行S_IRWXO
:其他读、写和执行S_IROTH
:用户读S_IWOTH
:用户写S_IXOTH
:用户执行chmod
函数更新的只是i
节点最近一次被修改的时间。chmod
函数在下列条件下自动清除两个权限位:- 如果我们试图设置普通文件的粘着位,而且又没有超级用户权限,则
mod
中的粘着位被自动关闭。这意味着只有超级用户才能设置普通文件的粘着位 - 新创建文件的组
ID
可能不是调用进程所属的组ID
,它可能是父目录的组ID
粘着位:如果对一个目录设置了粘着位,则任何对该目录具有写权限的进程都能够在该目录中创建文件。但是:只有满足下列条件之一的用户才能删除或者重命名该目录下的文件:
- 拥有此文件
- 拥有此目录
- 是超级用户
对于未设置粘着位的目录,则只要用户对该目录有写权限,那么就有修改和重命名该目录下其他文件的能力
修改用户的 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);
参数:
pathname
:文件路径名owner
:文件修改后的用户 IDgroup
:文件修改后的组 ID对于
fchown
函数:fd
:打开的文件描述符,要修改的就是这个文件对于
fchmod
函数:fd
:一个打开目录文件的描述符,或者AT_FDCWD
pathname
:- 如果为绝对路径,则忽略
fd
参数 - 如果为相对路径,则相对路径的目录由
fd
指定。- 若
fd=AT_FDCWD
,则表示相对于当前工作目录 - 否则相对于
fd
对于的打开的目录
- 若
- 如果为绝对路径,则忽略
flag
:如果是!AT_SYMLINK_FOLLOW
,则fchmodtat
并不跟随符号链接,修改的是符号链接本身而不是符号链接指向的文件
返回值:
- 成功: 返回 0
- 出错: 返回 -1
有两点注意:
lchown
函数更改的是符号链接本身,而chown
遇到符号链接时更改的是符号链接指向的文件- 如果这些函数由非超级用户进程调用,则成功返回时,该文件的设置用户 ID 和设置组 ID 位都被清除
示例:在
main
函数中调用test_chmod_chown
函数:void test_chmod_chown() { const char *file_name="test"; Stat buf; My_stat(file_name,&buf); My_chmod(file_name,S_IRWXU); My_chown(file_name,1,1); }
可以看到:
- 修改文件所属的用户和组,需要超级用户权限。普通用户无法修改,即使该用户就是该文件的所有者也不行
四、修改文件长度
文件长度:
stat.st_size
字段存放的是以字节为单位的文件的长度。此字段只对普通文件、目录文件、符号链接才有意义:- 对普通文件:其长度就是文件的大小。长度为 0 表示该文件为空
- 对目录文件:其长度通常是个整数(如 16 或者 512)的整数倍
对符号链接:其长度是符号链接本身存放的某个文件名的实际字节数(它并不包含字符串的
null
字节,因为这些字符是存放在文件中,而不是存放在内存中的字符串)另外
stat.st_blksize
存放的是对于文件 I/O 较合适的块长度;stat.st_blocks
存放的是所分配的块的数量(一个块 512 字节)。注意:- 对于普通文件,可能包含空洞。空洞是由于设置的文件偏移量超过了文件末尾,然后写入了某些数据造成的。对于空洞文件:
- 空洞文件的存储需要的磁盘块数量可能远小于文件大小。文件大小是文件末尾到文件头的字节数
- 读取空洞文件的空洞时,对于没有写过的字节位置
read
返回的是字节 0
截断文件:通常可以用带
O_TRUNC
选项的open()
函数来清空一个文件(截断到 0)。但是如果希望截断文件使得文件大小为指定字节数,则可以用下列的函数:#include<unistd.h> int truncate(const char*pathname,off_t length); int ftruncate(int fd,off_t length);
- 参数:
pathname
:文件路径名length
:文件修改后大小(字节数)fd
:打开的文件描述符,要修改的就是这个文件
返回值:
- 成功: 返回 0
- 出错: 返回 -1
有两点注意:
- 若
length
小于文件的原大小,则修改文件大小之后,文件新的尾端之后的位置不再可以访问 - 若
length
大于文件的原大小,则修改文件大小之后,会形成空洞。即从文件原大小新的尾端形成了空洞
- 参数:
示例:在
main
函数中调用test_truncate_size
函数:void test_truncate_size() { M_TRACE("--------- Begin test_truncate_size() ---------\n"); char buffer[100]; int len; int fd=My_open_with_mode("test",O_CREAT|O_TRUNC|O_RDWR,S_IRWXU); My_write(fd,"abcdefg",8); print_file_size("test"); // 打印文件大小 //**** 扩张文件 *******// My_truncate("test",20); // 扩张文件 My_lseek(fd,0,SEEK_SET); // 读取之前先调整文件读取位置 len=My_read(fd,buffer,20); printf("Read:"); for (int i=0;i<len;i++) // 打印读取内容 printf("\t0x%x,",buffer[i]); printf("\n"); //**** 截断文件 *******// My_truncate("test",5); // 截断文件 My_lseek(fd,0,SEEK_SET); // 读取之前先调整文件读取位置 len=My_read(fd,buffer,5); printf("Read:"); for (int i=0;i<len;i++) printf("\t0x%x,",buffer[i]); printf("\n"); close(fd); M_TRACE("--------- End test_truncate_size() ---------\n"); }
可以看到:
- 对于文件空洞,它不占用任何磁盘空间;空洞部分读出的内容全为 0
- 对于非常小的文件,比如这里的 8 字节文字,磁盘分配了 8 个块(4kb)。
五、UNIX 文件系统、硬链接、软链接、删除、重命名
UNIX 文件系统简介(传统的基于 BSD 的 UNIX 文件系统,称作
UFS
):- 一个磁盘可以划分成一个或者多个分区,每个分区可以包含一个文件系统。每个文件系统包含一些柱面组。每个柱面组包括:
- 一个 i 节点图:用于指示哪些 i 节点已经被使用,哪些未被使用
- 一个 块位图:用于指示哪些数据块已经被使用,哪些为被使用
- 一个 i 节点组。它包含的是许多 i 节点。
- 一个数据区:存放具体的数据块和目录块
- 数据区包含两种类型的块:
- 目录块:它的内容是
<i 节点编号>|<文件名>
这种格式的记录的列表 - 数据块:它的内容就是具体文件的数据
- 目录块:它的内容是
- i 节点是固定长度的记录项,它包含有关文件的大部分信息
- 每个 i 节点都有一个链接计数,其值是指向 i 节点的目录的项数(这种链接类型称之为硬链接)。只有当该链接计数减少为 0 时,才可以删除该链接文件(也就是释放该文件占用的数据块)。
- 在
stat
结构中,链接计数包含在st_nlink
成员中(POSIX 常量:LINK_MAX
指定了一个文件链接数的最大值)
- 在
- 每个 i 节点包含了文件有关的所有信息:文件类型、文件权限访问位、文件长度和指向文件数据块的指针
stat
结构中的大多数信息来自于 i 结点。只有两项重要数据存放在目录项中:文件名、i 节点编号
- 目录项中的 i 节点编号只能指向同一个文件系统中的相应的 i 节点。
因此硬链接不能跨文件系统
- 当在不更换文件系统的情况下重命名一个文件时,该文件的实际内容并未移动。只需要构造一个指向现有 i 节点的新目录项,并删除来的目录项。此时该 i 节点的链接计数不会改变
这就是
mv
命令的操作方式
- 每个 i 节点都有一个链接计数,其值是指向 i 节点的目录的项数(这种链接类型称之为硬链接)。只有当该链接计数减少为 0 时,才可以删除该链接文件(也就是释放该文件占用的数据块)。
- 一个磁盘可以划分成一个或者多个分区,每个分区可以包含一个文件系统。每个文件系统包含一些柱面组。每个柱面组包括:
与硬链接对应的概念是软链接。软链接也称作符号链接,它是一种特殊的文件。该文件的实际内容(在数据块中)包含了该符号链接所指向的文件的名字。同时该文件的 i 节点指示了该文件类型是
S_IFLNK
,于是系统知道了这个文件是个符号链接。- 硬链接直接指向文件的
i
节点 软链接是对一个文件的间接指针
引入符号链接的原因是为了避开硬链接的一些限制:
- 硬链接通常要求链接和文件位于同一个文件系统中
只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)
对于符号链接以及它指向何种类型的文件并没有什么限制。任何用户都可以创建指向目录的符号链接。但是使用符号链接有可能在文件系统中引入循环
对于处理文件和目录的函数,如果传递的是一个符号链接的文件名,则应该注意:函数是否跟随符号链接,即函数是处理符号链接指向的文件,还是处理符号链接本身。
跟随符号链接(即处理符号链接指向的文件)的函数有:
access
、chdir
、chmod
、chown
、creat
、exec
、link
、open
、opendir
、pathconf
、stat
、truncate
- 不跟随符号链接(即处理符号链接文件本身)的函数有:
lchown
、lstat
、readlink
、remove
、rename
、unlink
- 一个例外的情况:如果用
O_CREAT
和O_EXCL
选项调用open
,此时若参数是个符号链接的文件名,则open
出错返回(并不考虑符号链接指向的文件是否存在),同时将errno
设为EEXIST
- 一个例外的情况:如果用
- 硬链接直接指向文件的
任何一个目录
dirxxx
的硬链接至少为 2:- 该目录的内容中有一条名为的
.
记录,该记录的<i 节点编号>
指向dirxxx
目录的节点 - 该目录的父目录的内容中有一条记录,记录的名字
dirxxx
,记录的<i 节点编号>
指向dirxxx
目录的节点 - 若该目录有子目录。
dirxxx
的任何子目录的内容有一条名为..
的记录,该记录的<i 节点编号>
指向dirxxx
目录的节点因此父目录中的每个子目录都使得父目录的链接计数加 1
- 该目录的内容中有一条名为的
link/linkat
函数:创建一个指向现有文件的硬链接#include<unistd.h> int link(const char *existingpath,const char *newpath); int linkat(int efd,const char*existingpath,int nfd,const char *newpath,int flag);
参数:
existingpath
:现有的文件的文件名(新创建的硬链接指向它)newpath
:新创建的目录项- 如果
newpath
已存在,则返回出错 - 只创建
newpath
中的最后一个分量,路径中的其他部分应当已经存在。假设
newpath
为:/home/aaa/b/c.txt
,则要求/home/aaa/b
已经存在,只创建c.txt
对于
linkat
函数:- 如果
- 现有的文件名是通过
efd
和existingpath
指定。- 若
existingpath
是绝对路径,则忽略efd
- 若
existingpath
是相对路径,则:- 若
efd=AT_FDCWD
,则existingpath
是相对于当前工作目录来计算 - 若
efd
是一个打开的目录文件的文件描述符,则existingpath
是相对于efd
对应的目录文件
- 若
- 若
- 新建的文件名是通过
nfd
和newpath
指定。- 若
newpath
是绝对路径,则忽略nfd
- 若
newpath
是相对路径,则:- 若
nfd=AT_FDCWD
,则newpath
是相对于当前工作目录来计算 - 若
nfd
是一个打开的目录文件的文件描述符,则newpath
是相对于nfd
对应的目录文件
- 若
- 若
flag
:当现有文件是符号链接时的行为:flag=AT_SYMLINK_FOLLOW
:创建符号链接指向的文件的硬链接(跟随行为)flag=!AT_SYMLINK_FOLLOW
:创建符号链接本身的硬链接(默认行为)
返回值:
- 成功: 返回 0
- 失败: 返回 -1
这两个函数创建新目录项并对链接计数加 1。创建新目录项和增加链接计数是一个原子操作。
另外,大多数操作系统中,只有超级用户才能创建指向一个目录的硬链接,因为这样做很有可能在文件系统中形成循环。
unlink
函数:删除一个现有的目录项#include<unistd.h> int unlink(const char*pathname); int unlinkat(int fd,const char*pathname,int flag);
参数:
pathname
:现有的、待删除的目录项的完整路径名。对于
unlinkat
函数:- 现有的文件名是通过
fd
和pathname
指定。- 若
pathname
是绝对路径,则忽略fd
- 若
pathname
是相对路径,则:- 若
fd=AT_FDCWD
,则pathname
是相对于当前工作目录来计算 - 若
fd
是一个打开的目录文件的文件描述符,则pathname
是相对于fd
对应的目录文件
- 若
- 若
flag
:flag=AT_REMOVEDIR
:可以类似于rmdir
一样的删除目录flag=!AT_REMOVEDIR
:与unlink
执行同样的操作
返回值:
- 成功: 返回 0
- 失败: 返回 -1
为了解除对文件的链接,必须对包含该目录项的目录具有写和执行权限。如果还对该目录设置了粘着位,则对该目录必须具有写权限以及下列三个条件之一:
- 拥有该文件
- 拥有该目录
具有超级用户权限
这两个函数删除目录项并对链接计数减 1。创建新目录和增加链接计数是一个原子操作。
- 如果该文件的硬链接数不为 0, 则还可以通过其他链接访问该文件的内容
- 如果该文件的硬链接数为 0,而没有进程打开该文件,则该文件的内容才有被删除
- 如果该文件的硬链接数为 0,但是有进程打开了该文件,则该文件的内容不能被删除。当进程关闭文件时,内核会检查打开该文件的进程个数;当这个数量为 0,内核再去检查其链接计数。如果链接计数也是 0,则就删除该文件的内容。
这个特性常用于创建临时文件,先
open,create
一个文件,然后立即调用unlink
。这样即使程序崩溃,它所创建的临时文件也不会遗留下来 如果删除目录项出错,则不对该文件做任何更改
如果
pathname
是个符号链接,则unlink
删除该符号链接,而不会删除由该符号链接所引用的文件。如果仅仅给出符号链接的文件名,没有一个函数可以删除由该符号链接所引用的文件
如果文件系统支持,超级用户可以调用
unlink
,其参数pathname
指定一个目录通常推荐用
rmdir
函数,其语义更加清晰
link/unlink
实例:在main
函数中调用test_link_unlink
函数void test_link_unlink() { M_TRACE("--------- Begin test_link_unlink() ---------\n"); assert(prepare_file("test",NULL,0,S_IRWXU)==0); un_prepare_file("test1"); print_file_link_num("test"); My_link("test","test1"); My_unlink("test1"); print_file_link_num("test"); My_unlink("test1"); My_unlink("test"); print_file_link_num("test"); un_prepare_file("test"); un_prepare_file("test1"); M_TRACE("--------- End test_link_unlink() ---------\n\n"); }
可以看到:
test
和new_test
这两个文件共享一个 i 结点。因此该节点的 硬链接数为 2- 一旦删除
new_test
,则对new_test
执行fstatat
失败(因为已经被unlink
)。同时test
的硬链接数为 1 - 一旦
test
也被删除,则i
节点被释放。执行unlink
失败。
remove
函数:解除对一个目录或者文件的链接。#include<stdio.h> int remove(const char *pathname);
- 参数
pathname
:文件名或者目录名
返回值:
- 成功:返回 0
- 失败:返回 -1
对于文件,
remove
功能与unlink
相同;对于目录,remove
功能与rmdir
相同
- 参数
rename/renameat
函数:重命名文件或目录#inluce<stdio.h> int rename(const char*oldname,const char *newname); int renameat(int oldfd,const char*oldname,int newfd,const char* newname);
参数:
oldname
:现有的文件名或者目录名newname
:重命名的名字- 如果
oldname
是个文件名,则为该文件或者符号链接重命名。- 此时若
newname
已存在:若newname
是个目录则报错;若newname
不是个目录:则先将newname
目录项删除,然后将oldname
重命名为newname
- 此时若
newname
不存在:则直接将oldname
重命名为newname
- 此时若
- 如果
oldname
是个目录名,则为该目录重命名。- 此时若
newname
已存在:若newname
是个目录且该目录是个空目录,则先将它删除,然后oldname
重命名为newname
;若newname
是个目录且该目录不是个空目录,则报错;若newname
不是个目录,则报错 - 此时若
newname
不存在:则直接将oldname
重命名为newname
oldname
不能是newname
的前缀。因为重命名时,需要删除oldname
- 此时若
- 如果
oldname
或者newname
引用的是符号链接,则处理的是符号链接本身,而不是它引用的文件 - 不能对
.
和..
重命名。即.
和..
不能出现在oldname
和newname
的最后部分 - 若
newname
和oldname
引用同一个文件,则函数不作任何更改而成功返回
对于
renameat
函数:- 如果
- 现有的文件名或目录名是通过
oldfd
和oldname
指定。- 若
oldname
是绝对路径,则忽略oldfd
- 若
oldname
是相对路径,则:- 若
oldfd=AT_FDCWD
,则oldname
是相对于当前工作目录来计算 - 若
oldfd
是一个打开的目录文件的文件描述符,则oldname
是相对于oldfd
对应的目录文件
- 若
- 若
- 重命名的文件名或目录名是通过
newfd
和newname
指定。- 若
newname
是绝对路径,则忽略newfd
- 若
newname
是相对路径,则:- 若
newfd=AT_FDCWD
,则newname
是相对于当前工作目录来计算 - 若
newfd
是一个打开的目录文件的文件描述符,则newname
是相对于newfd
对应的目录文件
- 若
- 若
flag
:当现有文件是符号链接时的行为:flag=AT_SYMLINK_FOLLOW
:创建符号链接指向的文件的硬链接(跟随行为)flag=!AT_SYMLINK_FOLLOW
:创建符号链接本身的硬链接(默认行为)
返回值:
- 成功: 返回 0
- 失败: 返回 -1
对于包含
oldname
以及newname
的目录,调用进程必须具有写和执行的权限,因为将同时更改这两个目录。
symlink/symlinkat
函数:创建一个符号链接#include<unistd.h> int symlink(const char*actualpath,const char *sympath); int symlinkat(const char*actualpath,int fd,const char*sympath);
- 参数:
actualpath
:符号链接要指向的文件或者目录(可能尚不存在)sympath
:符号链接的名字二者不要求位于同一个文件系统中
- 参数:
对于`symlinkat`函数:
- 符号链接的名字是通过`fd`和`sympath`指定。
- 若`sympath`是绝对路径,则忽略`fd`
- 若 `sympath`是相对路径,则:
- 若 `fd=AT_FDCWD`,则`sympath`是相对于当前工作目录来计算
- 若 `fd`是一个打开的目录文件的文件描述符,则`sympath`是相对于`fd`对应的目录文件
- 返回值:
- 成功: 返回 0
- 失败: 返回 -1
readlink/readlinkat
函数:打开符号链接本身open
函数是跟随链接的,即打开符号链接指向的文件#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);
参数:
pathname
:符号链接的名字buf
:存放符号链接内容的缓冲区bufsize
:期望读入缓冲区的字节数对于
readlinkat
函数:符号链接的名字是通过
fd
和pathname
指定。- 若
pathname
是绝对路径,则忽略fd
- 若
pathname
是相对路径,则:- 若
fd=AT_FDCWD
,则pathname
是相对于当前工作目录来计算 - 若
fd
是一个打开的目录文件的文件描述符,则pathname
是相对于fd
对应的目录文件
- 若
- 若
返回值:
- 成功: 返回实际上读取的字节数
- 失败: 返回 -1
readlink
和readlinkat
函数组合了open、read、close
函数的所有操作。注意:读入
buf
中的符号链接的内容,并不是以null
字节终止。以
null
字节终止的是内存中的字符串这种数据结构。而符号链接文件的内容是简单的字符序列,并不是字符串。符号链接示例:在
main
函数中调用test_symlink_readlink
函数:void test_symlink_readlink() { M_TRACE("--------- Begin test_symlink_readlink() ---------\n"); assert(prepare_file("test","abcdefg0123456",14,S_IRWXU)==0); // 准备 test 文件 print_file_type("test"); // 查看 test 文件类型 My_symlink("test","test_symlink"); // 创建软连接 test_symlink 到 test print_file_type("test_symlink"); // 查看 test_symlink 文件类型 print_link_file("test_symlink"); // 由于 open 是链接跟随,所以这里打印 test 的内容 char buffer[128]; My_readlink("test_symlink",buffer,128); un_prepare_file("test"); // 删除 test 文件 un_prepare_file("test_symlink"); // 删除 test_symlink 文件 M_TRACE("--------- End test_symlink_readlink() ---------\n\n"); }
可以看到:
- 符号链接文件的内容就是它链接到的那个文件的绝对路径名,其中路径名字符序列不包含
null
字节 - 在
ubuntu 16.04
中,经多次测试,符号链接文件和普通文件的st_mode
完全相同。 open
一个链接文件,然后read
时发现读文件出错,原因是文件描述符有误(实际上打开文件时返回的文件描述符没问题)
六、修改文件的时间
文件的时间:在
stat
结构中存放着文件的三个时间:st_atim
:文件数据的最后访问时间st_mtim
:文件数据的最后修改时间st_ctim
: i 节点状态的最后更改时间关于这三个时间:
- 有很多操作,比如修改文件权限,修改文件的所有者等操作,他们只修改 i 节点状态(只影响
st_ctim
),但是并不修改文件数据,也并不访问文件数据 - 系统并不维护对
i
节点的最后访问时间。因此对于access
函数和stat
函数,他们并不修改这三个时间中的任何一个 - 创建一个文件不仅影响了文件本身的这三个时间,也会影响该文件目录的这三个时间
futimens/utimensat/utimes
函数:修改文件的访问和修改时间#include<sys/stat.h> int futimens(int fd,const struct timespec times[2]); int utimensat(int fd,const char*path,const struct timespec times[2],int flag); #include<sys/time.h> int utimes(const char*pathname,const struct timeval times[2]);
参数:
对于
futimens
和utimensat
函数:times
:指向待修改文件的指定的文件数据访问和文件数据修改时间的指针。对于 C 语言,参数中的数组自动转换为指向数组的指针
- 这两个时间是日历时间,是自 1970:01:01--00:00:00 以来经历的秒数。不足秒的部分用纳秒表示
- 数组的第一个元素指定
st_atim
;数组的第二个元素指定st_ctim
times
可以按照下列四种方式之一指定:times
为空指针: 则将文件的数据访问时间和文件数据修改时间设置为当前时间此时要求进程的有效用户 ID 等于该文件所有者的 ID;或者进程对该文件有写权限;或者进程是个超级用户进程
times
参数是指向timespec
数组的指针:- 若数组的任何一个元素的
tv_nsec
字段为UTIME_NOW
,则相应的时间戳就设置为当前时间,忽略相应的tv_sec
字段此时要求进程的有效用户 ID 等于该文件所有者的 ID;或者进程对该文件有写权限;或者进程是个超级用户进程
- 若数组的任何一个元素的
tv_nsec
字段为UTIME_OMIT
,则相应的时间戳保持不变,忽略相应的tv_sec
字段若两个时间戳都忽略,则不需要任何权限限制
- 若数组的任何一个元素的
tv_nsec
字段为不是上面的两种之一,则相应的时间戳就设置为相应的tv_sec
和tv_nsec
字段此时要求进程的有效用户 ID 等于该文件所有者的 ID;或者进程是个超级用户进程(对文件只有写权限是不够的)
- 若数组的任何一个元素的
对于
utimes
函数:pathname
:文件的路径名times
:指向timeval
数组的指针。timeval
结构用秒和微秒表示。struct timeval{ time_t tv_sec;//秒 long tv_usec; //微秒 };
对于
futimens
函数:fd
:待修改文件的打开的文件描述符对于
utimensat
函数:- 待打开文件的名字是通过
fd
和path
指定。- 若
path
是绝对路径,则忽略fd
- 若
path
是相对路径,则:- 若
fd=AT_FDCWD
,则path
是相对于当前工作目录来计算 - 若
fd
是一个打开的目录文件的文件描述符,则path
是相对于fd
对应的目录文件
- 若
- 若
flag
:若待修改的文件是符号链接- 如果为
!AT_SYMLINK_FOLLOW
,则符号链接本身的时间就会被修改 - 默认情况下,修改的是符号链接指向的文件的时间(跟随行为)
- 如果为
返回值:
- 成功: 返回 0
- 失败: 返回 -1
我们不能对
st_ctim
(i 节点最后被修改时间)指定一个值。这个时间是被自动更新的。
示例:在
main
函数中调用test_utimes
函数:void test_utimes() { M_TRACE("--------- Begin test_utimes() ---------\n"); assert(prepare_file("test",NULL,0,S_IRWXU)==0); // 准备 test 文件 print_file_time("test"); sleep(2); My_access("test",F_OK); // 访问文件,但不修改文件 print_file_time("test"); sleep(2); My_chmod("test",S_IRUSR|S_IWUSR);// 修改文件状态 print_file_time("test"); struct timeval times[2]; times[0].tv_usec=10; times[1].tv_sec=10; times[1].tv_usec=10; My_utimes("test",times); un_prepare_file("test"); // 删除 test 文件 M_TRACE("--------- End test_utimes() ---------\n\n"); }
可以看到:st_ctim
是由系统自动维护的,程序员无法手动指定
七、目录操作
mkdir/mkdirat
函数创建一个空目录:#include<sys/stat.h> int mkdir(const char*pathname,mode_t mode); int mkdirat(int fd,const char *pathname,mode_t mode);
参数:
pathname
:被创建目录的名字mode
:被创建目录的权限对于
mkdirat
,被创建目录的名字是由fd
和pathname
共同决定的。- 若
pathname
是绝对路径,则忽略fd
- 若
pathname
是相对路径,则:- 若
fd=AT_FDCWD
,则pathname
是相对于当前工作目录来计算 - 若
fd
是一个打开的目录文件的文件描述符,则pathname
是相对于fd
对应的目录文件
- 若
返回值:
- 成功: 返回 0
- 失败: 返回 -1
注意:
- 他们创建的目录是空目录。
- 对于目录,通常至少要设置一个执行权限位,以允许访问该目录中的文件名
rmdir
函数:删除一个空目录#include<unistd.h> int rmdir(const char *pathname);
参数:
pathname
:待删除的空目录的名字
返回值:
- 成功: 返回 0
- 失败: 返回 -1
如果调用此函数使得目录的链接计数为 0 时:
- 如果此时没有其他进程打开该目录,则释放由此目录占用的空间。
- 如果此时有一个或者多个进程打开此目录,则在此函数返回时删除最后一个链接以及
.
和..
项,直到最后一个打开该目录的进程关闭该目录时此目录才真正被释放。- 此时,在此目录中不能再创建新文件。
读、写目录:对于某个目录具有访问权限的任何用户都可以读该目录。但是为了防止文件系统产生混乱,只有内核才能写目录。
一个目录的写权限和执行权限位决定了在该目录中能否创建新文件以及删除文件,它们并不能写目录本身
#include<dirent.h> DIR *opendir(const char *pathname); DIR *fdopendir(int fd); struct dirent *readdir(DIR *dp); void rewinddir(DIR *dp); int closedir(DIR *dp); long telldir(DIR *dp); void seekdir(DIR *dp,long loc);
各个函数:
opendir
:打开目录。- 参数:
pathname
:目录的名字 - 返回值:成功返回目录指针;失败返回
NULL
- 参数:
fdopendir
:打开目录。- 参数:
fd
:目录文件的文件描述符 - 返回值:成功返回目录指针;失败返回
NULL
- 参数:
readdir
:读取目录- 参数:
dp
:目录指针 - 返回值: 成功则返回目录项的指针;失败返回
NULL
- 参数:
rewinddir
:将目录的文件偏移量清零(这样下次读取就是从头开始)- 参数:
dp
:目录指针
- 参数:
closedir
:关闭目录。- 参数:
dp
:目录指针 - 返回值:成功返回 0 ;失败返回 -1
- 参数:
telldir
:返回目录的文件偏移量- 参数:
dp
:目录指针 - 返回值:成功返回目录的文件偏移量 ;失败返回 -1
- 参数:
seekdir
:设置目录的当前位置- 参数:
dp
:目录指针;loc
:要设定的文件偏移量
对于
DIR
结构,它是一个内部结构。起作用类似于FILE
结构。 对于dirent
结构,它是定义在<dirent.h>
头文件中。其与具体操作系统相关。但是它至少定义了两个成员:struct dirent{ ino_t d_ino; // i 节点编号 char d_name[];// 以 null 结尾的文件名字符串 }
d_name
项的大小并没有指定,但必须保证它能包含至少NAME_MAX
个字节(不包含终止null
字节)目录中各目录项的顺序与操作系统有关。它们通常不按照字母顺序排列
- 参数:
当前工作目录:每个进程都有一个当前工作目录。此目录是搜索所有相对路径名的起点。
当前工作目录是本进程的一个属性
与当前工作目录相关的有三个函数:
#include<unistd.h> int chdir(const char *pathname); int fchdir(int fd); char *getcwd(char *buf,size_t size);
各个函数:
chdir
:更改当前工作目录。- 参数:
pathname
:将该目录作为当前工作目录 - 返回值:成功返回 0 ;失败返回 -1
- 参数:
fchdir
:更改当前工作目录。- 参数:
fd
:将该fd
文件描述符对应的目录作为当前工作目录 - 返回值:成功返回 0 ;失败返回 -1
- 参数:
getcwd
:返回当前工作目录的名字- 参数:
buf
:缓冲区地址;size
:缓冲区长度。这两个参数决定了当前工作目录名字字符串存放的位置。缓冲区必须足够长以容纳绝对路径名加上一个终止
null
字节。否则返回出错。 - 返回值: 成功则返回
buf
;失败返回NULL
- 参数:
示例: 在
main
函数中调用test_dir_operations
函数:void test_dir_operations() { M_TRACE("--------- Begin test_dir_operations() ---------\n"); //*** 创建目录 **** My_mkdir("test",S_IRWXU); My_mkdir("test/test1",S_IRWXU); //*** 创建文件 prepare_file("test/tfile_1",NULL,0,S_IRWXU); prepare_file("test/tfile_2",NULL,0,S_IRWXU); prepare_file("test/tfile_3",NULL,0,S_IRWXU); prepare_file("test/test1/tfile_11",NULL,0,S_IRWXU); prepare_file("test/test1/tfile_22",NULL,0,S_IRWXU); prepare_file("test/test1/tfile_33",NULL,0,S_IRWXU); print_dir("test"); print_cwd(); My_chdir("test"); print_cwd(); My_chdir("../"); // 切换回来,否则后面的删除文件都会失败(因为都是相对路径) print_cwd(); //***** 清理 My_rmdir("test"); // 目录非空,删除失败! un_prepare_file("test/tfile_1"); un_prepare_file("test/tfile_2"); un_prepare_file("test/tfile_3"); un_prepare_file("test/test1/tfile_11"); un_prepare_file("test/test1/tfile_22"); un_prepare_file("test/test1/tfile_33"); My_rmdir("test/test1"); // 必须非空才能删除成功 My_rmdir("test"); // 必须非空才能删除成功 M_TRACE("--------- End test_dir_operations() ---------\n\n"); }
- 符号链接文件的内容就是它链接到的那个文件的绝对路径名,其中路径名字符序列不包含
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论