APUE学习笔记之文件与目录(四)

本文记录《UNIX环境高级编程》第3版中第4章文件与目录的一些知识点。

本章将描述文件系统的其他特征和文件的性质。将从stat函数开始,逐个说明stat结构的每一个成员以了解文件的所有属性。在此过程中,将说明修改这些属性的各个函数(更改所有者、更改权限等),还将更详细地说明UNIX文件系统的结构以及符号链接。本章最后介绍对目录进行操作的各个函数,并且开发了一个以降序遍历目录层次结构的函数。


函数stat、fstat、fstatat和lstat

1
2
3
4
5
6
#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);
// 所有4个函数的返回值:若成功;返回0;若出错,返回-1

一旦给出pathname,stat函数将返回与此命名文件有关的信息结构。fstat函数获得已在描述符fd上打开文件的有关信息。lstat函数类似于stat,但是当命名的文件是一个符号链接时,lstat返回该符号链接的有关信息,而不是由该符号链接引用的文件的信息。

参数buf是一个指针,它指向一个我们必须提供的结构。函数来填充由buf指向的结构。结构的实际定义可能随具体实现有所不同,但其基本形式是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/stat.h>
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512 B blocks allocated
/* Since POSIX.1-2008, this structure supports nanosecond
precision for the following timestamp fields.
For the details before POSIX.1-2008, see VERSIONS.
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};

timespec结构类型按照秒和纳秒定义了时间,至少包括下面两个字段:

1
2
time_t tv_sec;
long tv_nsec;

文件类型

文件类型包括如下几种:

(1)普通文件(regular file)。

(2)目录文件(directory file)。这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件。进程必须使用本章介绍的函数才能更改目录。

(3)块特殊文件(block special file)。这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。

(4)字符特殊文件(character special file)。这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。

(5)FIFO。这种类型的文件用于进程间通信,有时也称为命名管道(named pipe)。

(6)套接字(socket)。这种类型的文件用于进程间的网络通信。套接字也可用于在一台宿主机上进程之间的非网络通信。

(7)符号链接(symbolic link)。这种类型的文件指向另一个文件。

文件类型信息包含在stat结构的st_mode成员中。可以用下图中的宏确定文件类型。这些宏的参数都是stat结构中的st_mode成员。

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

设置用户ID和设置组ID

与一个进程相关联的ID有6个或更多,如图所示。

img

  • 实际用户ID和实际组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)位

例如,若文件所有者是超级用户,而且设置了该文件的设置用户ID位,那么当该程序文件由一个进程执行时,该进程具有超级用户权限。不管执行此文件的进程的实际用户ID是什么,都会是这样。例如,UNIX系统程序passwd允许任一用户改变其口令,该程序是一个设置用户ID程序。因为该程序应能将用户的新口令写入口令文件中(一般是/etc/passwd 或/etc/shadow),而只有超级用户才具有对该文件的写权限,所以需要使用设置用户ID功能。因为运行设置用户ID程序的进程通常会得到额外的权限,所以编写这种程序时要特别谨慎。

再回到stat函数,设置用户ID位及设置组ID位都包含在文件的st_mode值中。这两位可分别用常量S_ISUIDS_ISGID测试。


文件访问权限

st_mode值也包含了对文件的访问权限位。所有文件类型都有访问权限。

每个文件有9个访问权限位,可将它们分成3类,见下图。

9个访问权限位,取自`<sys/stat.h>`”
style=“zoom:67%;” /></p>
<p>前3行中,术语用户指的是文件所有者(owner)。<code>chmod</code>命令用于修改这9个权限位。该命令允许用<code>u</code>表示用户(所有者),用<code>g</code>表示组,用<code>o</code>表示其他。</p>
<p>图中的3类访问权限(即读、写及执行)以各种方式由不同的函数使用。我们将这些不同的使用方式汇总在下面。</p>
<ul>
<li><p>第一个规则是,用名字打开任一类型的文件时,对包含该名字的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。例如,为了打开文件<code>/usr/include/stdio.h</code>,需要对目录<code>/</code>、<code>/usr</code>和<code>/usr/include</code>具有执行权限。然后,需要具有对文件本身的适当权限,这取决于以何种模式打开它(只读、读-写等)。</p>
<blockquote>
<p>注意,对于目录的读权限和执行权限的意义是不相同的。读权限允许读目录,获得在该目录中所有文件名的列表。当一个目录是我们要访问文件的路径名的一个组成部分时,对该目录的执行权限使我们可通过该目录(也就是搜索该目录,寻找一个特定的文件名)。</p>
</blockquote></li>
<li><p>对于一个文件的读权限决定了是否能够打开现有文件进行读操作。这与<code>open</code>函数的<code>O_RDONLY</code>和<code>O_RDWR</code>标志相关。</p></li>
<li><p>对于一个文件的写权限决定了是否能够打开现有文件进行写操作。这与<code>open</code>函数的<code>O_WRONLY</code>和<code>O_RDWR</code>标志相关。</p></li>
<li><p>为了在<code>open</code>函数中对一个文件指定<code>O_TRUNC</code>标志,必须对该文件具有写权限。</p></li>
<li><p>为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。</p></li>
<li><p>为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限。对该文件本身则不需要有读、写权限。</p></li>
<li><p>如果用7个exec函数中的任何一个执行某个文件,都必须对该文件具有执行权限。该文件还必须是一个普通文件。</p></li>
</ul>
<p>进程每次打开、创建或删除一个文件时,内核就进行文件访问权限测试,而这种测试可能涉及文件的所有者(st_uid和st_gid)、进程的有效ID(有效用户ID和有效组ID)以及进程的附属组ID(若支持的话)。两个所有者ID是文件的性质,而两个有效ID和附属组ID则是进程的性质。内核进行的测试具体如下。</p>
<p>(1)若进程的有效用户ID是0(超级用户),则允许访问。这给予了超级用户对整个文件系统进行处理的最充分的自由。</p>
<p>(2)若进程的有效用户ID等于文件的所有者ID(也就是进程拥有此文件),那么如果所有者适当的访问权限位被设置,则允许访问;否则拒绝访问。适当的访问权限位指的是,若进程为读而打开该文件,则用户读位应为1;若进程为写而打开该文件,则用户写位应为1;若进程将执行该文件,则用户执行位应为1。</p>
<p>(3)若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果组适当的访问权限位被设置,则允许访问;否则拒绝访问。</p>
<p>(4)若其他用户适当的访问权限位被设置,则允许访问;否则拒绝访问。</p>
<hr />
<h2 id=新文件和目录的所有权

新文件的用户ID设置为进程的有效用户ID。关于组ID,POSIX.1允许实现选择下列之一作为新文件的组ID。

(1)新文件的组ID可以是进程的有效组ID。

(2)新文件的组ID可以是它所在目录的组ID。


函数access和faccessat

正如前面所说,当用open函数打开一个文件时,内核以进程的有效用户ID和有效组ID为基础执行其访问权限测试。有时,进程也希望按其实际用户ID和实际组ID来测试其访问能力。accessfaccessat函数是按实际用户ID和实际组ID进行访问权限测试的。(该测试也分成4步,这与上节中所述的一样,但将有效改为实际。)

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

函数umask

umask函数为进程设置文件模式创建屏蔽字,并返回之前的值。(这是少数几个没有出错返回函数中的一个。)

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

其中,参数cmask是由9个常量(S_IRUSRS_IWUSR等)中的若干个按位或构成。

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

用户可以设置umask值以控制他们所创建文件的默认权限。该值表示成八进制数。设置了相应位后,它所对应的权限就会被拒绝。常用的几种umask值是002、022和027。002阻止其他用户写入你的文件,022阻止同组成员和其他用户写入你的文件,027阻止同组成员写你的文件以及其他用户读、写或执行你的文件。


函数chmod、fchmod和fchmodat

这3个函数可以更改现有文件的访问权限。

1
2
3
4
5
#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

为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限

参数mode是下图中所示常量的按位或。

`chmod`函数的mode常量,取自`<sys/stat.h>`”
style=“zoom: 50%;” /></p>
<p>注意,在图中,另外加了6个权限位,它们是两个设置ID常量(<code>S_ISUID</code>和<code>S_ISGID</code>)、保存正文常量(<code>S_ISVTX</code>)以及3个组合常量(<code>S_IRWXU</code>、<code>S_IRWXG</code>和<code>S_IRWXO</code>)。</p>
<hr />
<h2
id=函数chown、fchown、fchownat和lchown

下面几个chown函数可用于更改文件的用户ID和组ID。如果两个参数owner或group中的任意一个是-1,则对应的ID不变。

1
2
3
4
5
6
#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

文件截断

1
2
3
4
#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(也就是可能在文件中创建了一个空洞)。


函数link、linkat、unlink、unlinkat和remove

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

1
2
3
4
#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);
// 两个函数的返回值:若成功,返回0;若出错,返回-1

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

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

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

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

也可以用 remove 函数解除对一个文件或目录的链接。对于文件,remove 的功能与unlink相同。对于目录,remove的功能与rmdir相同。

1
2
3
#include <stdio.h>
int remove(const char *pathname);
// 返回值:若成功,返回0;若出错,返回-1

函数rename和renameat

文件或目录可以用rename函数或者renameat函数进行重命名。

1
2
3
4
#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

创建和读取符号链接

可以用symlink或symlinkat函数创建一个符号链接。

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

因为open函数跟随符号链接,所以需要有一种方法打开该链接本身,并读该链接中的名字。readlink和readlinkat函数提供了这种功能。

1
2
3
4
#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);
// 两个函数的返回值:若成功,返回读取的字节数;若出错,返回-1

两个函数组合了 open、read 和 close 的所有操作。如果函数成功执行,则返回读入buf的字节数。在buf中返回的符号链接的内容不以null字节终止。


文件的时间

注意,修改时间(st_mtim)和状态更改时间(st_ctim)之间的区别。修改时间是文件内容最后一次被修改的时间。状态更改时间是该文件的i节点最后一次被修改的时间。

image-20250220120341549


函数futimens、utimensat和utimes

一个文件的访问和修改时间可以用以下几个函数更改。futimens和utimensat函数可以指定纳秒级精度的时间戳。用到的数据结构是与stat函数族相同的timespec结构。

1
2
3
4
#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);
// 两个函数返回值:若成功,返回0;若出错,返回-1

这两个函数的times数组参数的第一个元素包含访问时间,第二元素包含修改时间。这两个时间值是日历时间,这是自特定时间(1970年1月1日00:00:00)以来所经过的秒数。不足秒的部分用纳秒表示。

时间戳可以按下列4种方式之一进行指定。

(1)如果times参数是一个空指针,则访问时间和修改时间两者都设置为当前时间。

(2)如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳就设置为当前时间,忽略相应的tv_sec字段。

(3)如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_OMIT,相应的时间戳保持不变,忽略相应的tv_sec字段。

(4)如果 times 参数指向两个 timespec 结构的数组,且 tv_nsec 字段的值为既不是UTIME_NOW 也不是 UTIME_OMIT,在这种情况下,相应的时间戳设置为相应的 tv_sec 和tv_nsec字段的值。

futimens 函数需要打开文件来更改它的时间,utimensat 函数提供了一种使用文件名更改文件时间的方法。pathname参数是相对于fd参数进行计算的,fd要么是打开目录的文件描述符,要么设置为特殊值 AT_FDCWD(强制通过相对于调用进程的当前目录计算pathname)。如果pathname指定了绝对路径,那么fd参数被忽略。

utimensat的flag参数可用于进一步修改默认行为。如果设置了AT_SYMLINK_NOFOLLOW标志,则符号链接本身的时间就会被修改(如果路径名指向符号链接)。默认的行为是跟随符号链接,并把文件的时间改成符号链接的时间。

futimens 和utimensat 函数都包含在POSIX.1 中,第3 个函数utimes 包含在Single UNIX Specification的XSI扩展选项中。

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

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

1
2
3
4
struct timeval {
time_t tv_sec; /* seconds */
long tv_usec; /* microseconds */
};

注意,不能对状态更改时间st_ctim(i节点最近被修改的时间)指定一个值,因为调用utimes函数时,此字段会被自动更新。


函数mkdir、mkdirat和rmdir

用mkdir和mkdirat函数创建目录,用rmdir函数删除目录。

1
2
3
4
#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函数可以删除一个空目录。空目录是只包含.和..这两项的目录。

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

读目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
//两个函数返回值:若成功,返回指针;若出错,返回NULL

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

void rewinddir(DIR *dp);

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

long telldir(DIR *dp);
// 返回值:与dp关联的目录中的当前位置

void seekdir(DIR *dp, long loc);

由opendir和fdopendir返回的指向DIR结构的指针由另外5个函数使用。opendir执行初始化操作,使第一个readdir返回目录中的第一个目录项。DIR结构由fdopendir创建时,readdir返回的第一项取决于传给fdopendir函数的文件描述符相关联的文件偏移量。注意,目录中各目录项的顺序与实现有关。它们通常并不按字母顺序排列。


函数chdir、fchdir和getcwd

每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜线开始的路径名为相对路径名)。当用户登录到 UNIX 系统时,其当前工作目录通常是口令文件(/etc/passwd)中该用户登录项的第6个字段—用户的起始目录(home directory)。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。

进程调用chdir或fchdir函数可以更改当前工作目录。

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

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

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

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


习题

4.6 编写一个类似cp的程序,它复制包含空洞的文件,但不将字节0写到输出文件中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

#define BUF_SIZE 4096 // 缓冲区大小

// 复制文件,跳过空洞
int copy_file_skip_holes(const char *src, const char *dst) {
int src_fd = open(src, O_RDONLY);
if (src_fd == -1) {
perror("打开源文件失败");
return -1;
}

int dst_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (dst_fd == -1) {
perror("打开目标文件失败");
close(src_fd);
return -1;
}

char buf[BUF_SIZE];
ssize_t bytes_read, bytes_written;

while ((bytes_read = read(src_fd, buf, BUF_SIZE)) > 0) {
// 跳过全为零的部分(空洞)
for (ssize_t i = 0; i < bytes_read; ++i) {
if (buf[i] != 0) {
// 只写非零字节
bytes_written = write(dst_fd, &buf[i], 1);
if (bytes_written != 1) {
perror("写入目标文件失败");
close(src_fd);
close(dst_fd);
return -1;
}
}
}
}

if (bytes_read == -1) {
perror("读取源文件失败");
close(src_fd);
close(dst_fd);
return -1;
}

close(src_fd);
close(dst_fd);
return 0;
}

int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "用法: %s 源文件 目标文件\n", argv[0]);
return 1;
}

if (copy_file_skip_holes(argv[1], argv[2]) == -1) {
return 1;
}

printf("文件复制完成\n");
return 0;
}

// 生成空洞文件
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
// 打开文件并准备写入
int fd = open("hole_file", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("打开文件失败");
return 1;
}

// 写入 "hello world"
if (write(fd, "hello world", 11) != 11) {
perror("写入文件失败");
close(fd);
return 1;
}

// 创建空洞,通过 lseek 跳到 1MB 位置
if (lseek(fd, 1024 * 1024, SEEK_SET) == -1) {
perror("lseek 失败");
close(fd);
return 1;
}

// 在空洞之后写入 "hello again"
if (write(fd, "hello again", 12) != 12) {
perror("写入文件失败");
close(fd);
return 1;
}

// 关闭文件
close(fd);
printf("成功创建空洞文件:hole_file\n");
return 0;
}