阅读分享 -- 《Linux 系统编程 第二版》I

思来想去,还是先从阅读分享开始吧,系统编程内容暂时还没有头绪...

概述

这本书大致分为文件、进程、线程、内存、IPC通讯、Linux时钟,这六大部分。下面时大致的概念归纳(本文内容展开也将按此顺序展开)

  • 文件(1-4章、第8章) - Linux 的文件概念 - 文件基本IO(包含字符流/文本流/字节流/文件属性/目录/设备文件) - 高级IO(磁盘IO) - 多路IO复用
  • 进程(5-6章) - 进程概念 - 进程管理 - 高级进程管理
  • 线程
  • 内存
  • IPC 通讯 - 并发 - 资源/数据竞争
  • Linux时钟

chapter I 文件

Linux 文件概念

Linux 下一切皆文件。

Linux 下可分文普通文件和特殊文件:

  • 一般文件:字符流文件[比如文本文件]
  • 特殊文件:块设备文件,字符设备文件,命名管道,Unix套接字文件

Linux 文件系统存储原理

image-dqcl.png

Linux 同过VFS虚拟文件系统对上提供了一个统一的接口使得使用者无需关心底层存储系统是那一中文件系统格式,各个格式的系统可以以插件的形式嵌入到虚拟文件系统中。以下是文件Linux文件存储的简易示意图

image-pliu.png

image-aley.png

image-lnza.png

Linux 文件系统中并不以文件名问文件的为以查找索引,而是使用表加序列映射(多级索引表)的方式去管理和存储数据也就是inode。

  • super blk

super block

存储了文件系统的元信息 -- Linux 要求接入VFS 的文件系统都需要实现此模块,在不在磁盘中驻留的RAM 文件系统在也须生成此模块(一般在生成时生成此模块)

eg: 文件系统的大小、块大小、inode 数量、挂载次数等

还存储了文件系统的状态信息,以便在系统崩溃或意外关机时进行文件系统检测和修复

  • 块组描述符
  • block bitmap

这个块位图用于跟踪该块组中每个数据块的使用情况, 用于维护文件系统的空间使用情况

块位图中的每一个 bit 对应一个数据块

如果位为 1,则表示对应的数据块已被使用

如果位为 0,则表示对应的数据块为空闲

  • inode bitmap

用于跟踪该块组中每个 inode 的使用情况

inode 位图中的每一位对应一个 inode

如果位为 1,则表示对应的 inode 已被使用;如果位为 0,则表示对应的 inode 为空闲

  • inode table

inode 表,使用inode 号查表可获得对应结构体地址

  • inode blk

index node block

  • data blk

data block

打开一个文件发生了什么?

ext4 文件系统 inode 结构提部分源码

// linux-5.4.18/fs/ext4/ext4.h

/*
 * Structure of an inode on the disk
 */
struct ext4_inode {
	__le16	i_mode;		/* File mode */
	__le16	i_uid;		/* Low 16 bits of Owner Uid */
	__le32	i_size_lo;	/* Size in bytes */
	__le32	i_atime;	/* Access time */
	__le32	i_ctime;	/* Inode Change time */
	__le32	i_mtime;	/* Modification time */
	__le32	i_dtime;	/* Deletion Time */
	__le16	i_gid;		/* Low 16 bits of Group Id */
	__le16	i_links_count;	/* Links count */
	__le32	i_blocks_lo;	/* Blocks count */
	__le32	i_flags;	/* File flags */
	......
	__le32	i_block[EXT4_N_BLOCKS];/* Pointers to blocks */

Linux 直接管理文件名所建立的映射项,在Linux 世界下目录才是文件管理直接项(根目录也是如此)[目录下包含文件 - dir/file],这个目录文件存储在对应的datablock中。

目录项的索引是通过目录文件内部的哈希表(hash table)或 B+ 树实现的。当用户打开一个目录时,系统会读取目录文件中的目录项,并将它们缓存在内存中。当用户访问某个文件时,系统会根据文件名查找对应的目录项,获取该文件的 inode 号,然后根据 inode 号读取文件的内容。

用户在打开一个文件时系统做了一下几步工作:

  1. 在缓存在内存中目录属下对应文件名的inode
  2. 用inode号查inode table 读取对应inode结构体数据(元信息),做权鉴,若有权限则进行下一步
  3. 在元信息块中拿到datablk0地址
  4. 通过data blk0 中的地址寻址载入并访问磁盘中的数据

其他事项:

  • i_block 数组存储了指向文件数据块的指针,用于定位和访问文件的实际数据。
  • inode 也能够将数据存储在 inode 本身 i_block 数组中。这叫做 Inlining。这种存储方法具有节省空间的优点,因为不需要数据块。它还通过避免更多的磁盘访问来获取数据,从而增加了查找时间。
  • 像 ext4 这样的一些文件系统有一个名为 inline_data 的选项。启用后,它允许操作系统以这种方式存储数据。由于大小限制,内联只适用于非常小的文件。如果大小不超过 60 字节,Ext2 和更高版本通常会以这种方式存储软链接信息。

内嵌存储(Inlining)是一种特定文件系统(如 ext4)提供的功能,它允许将小量的数据直接存储在 inode 本身,而无需额外的数据块。这种存储方式具有以下特点:
(1)节省空间:通过在 inode 内部直接存储数据,文件系统避免了分配额外的数据块的需要。这可以节省空间,特别是对于数据量较小的文件。它消除了为这些文件分配和管理单独的数据块的开销。

(2)减少磁盘访问:使用内嵌存储,数据可以直接从 inode 中访问,而无需进行额外的磁盘读取。这可以提高小文件的查找速度,因为数据直接在 inode 结构中可用。

(3)大小限制:内嵌存储有一定的大小限制,因为 inode 具有固定的大小。可以内嵌存储的数据量取决于具体的文件系统及其配置。例如,在 ext4 中,inline_data 选项允许将小文件内嵌存储,通常限制在 60 字节以内。如果文件超过大小限制,则会回退到传统的使用数据块的方式。

(4)使用场景:内嵌存储通常用于存储小文件,例如配置文件、小型脚本或符号链接信息。这些文件通常符合内嵌存储的大小限制,并从其节省空间和提高性能的优势中受益。

Inode 软链接、硬链接

ln -s [path_source] [path_target]
#创建软连接 会在磁盘生成(创建)一个有软连接信息的软连接指向文件,内容记录的是源文件的路径,这个文件有独立的inode号
#软连接可以跨文件系统,可以指向文件或者目录

---------------------------------------------------
ln [path_source] [path_target]
#创建硬链接,硬链接没有独立inode 因此没有独立的datablk块,只是在目标目录数据块文件中中新增了一条文件路径
#硬链接只能指向文件,不能跨文件系统
#创建硬链接后源文件的元信息中的引用计数会+1,删除对应硬链接计数会-1,当计数为0,时表示该文件被删除,对应的blk bitmap 项也会被置0

关于文件的骚操作

df -i path_name
touch path_name/{1..100000}.txt
#创建过多的空文件,使该目录下的inode 号配合全被创建的空文件inode号占满,使得无法在此创建新文件。
#某些特殊文件名的文件使用rm 文件名无法删除时可以使用如下:
ls -li
find -inum [inode_num] -exec rm -i {} /;

文件基本I/O 1-4章、第8章

字符流/文本流/字节流/文件属性/目录/设备文件

Errno

Linux 系统中,errno 是一个全局变量,用于表示最近一次系统调用或库函数调用出错时的错误码。每个错误码对应一个特定的错误类型。了解这些错误码有助于调试和处理错误情况。

要使用 errno,需要包含头文件 #include <errno.h>

Linux 系统调用或库函数在出错时会自动设置 errno 为相应的错误码。可以在调用函数后检查 errno 的值来确定错误类型,通过 perror() 或 strerror() 函数将错误码转换为可读的错误消息。

一下为错误示例和正确示例:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main(int argc,char* argvs[]){
    ...
    // 错误示例
    if(fsync(fd) == -1){
    fprintf(stderr,"fsync fail %s\n",strerror(errno)); // errno 是线程安全的,每一个线程都有一个全局可见的errno,此处fprintf(...) 调用若出现错误则有可能会将原先fsync(...) 调用产生的错误替换掉故打印出来的消息可能不是开发者期待看到的fsync 的errno 对应的错误消息字符串。
    if(errno == EIO) fprintf(stderr,"I/O error on %d\n",fd);
    }

    // 正确示例
    if(fsync(fd) == -1){
    const err = errno;
    fprintf(stderr,"fsync fail %s\n",strerror(err));
    if(errno == EIO) fprintf(stderr,"I/O error on %d\n",fd);
    exit(EXIT_FAILURE);
    }
}

Errno 对照表

在头文件「/usr/include/asm-generic/errno-base.h」中对基础的常用 errno 进行了宏定义:

define errno explain
EPERM 1 Operation not permitted
ENOENT 2 No such file or directory
ESRCH 3 No such process
EINTR 4 Interrupted system call
EIO 5 I/O error
ENXIO 6 No such device or address
E2BIG 7 Argument list too long
ENOEXEC 8 Exec format error
EBADF 9 Bad file number
ECHILD 10 No child processes
EAGAIN 11 Try again
ENOMEM 12 Out of memory
EACCES 13 Permission denied
EFAULT 14 Bad address
ENOTBLK 15 Block device required
EBUSY 16 Device or resource busy
EEXIST 17 File exists
EXDEV 18 Cross-device link
ENODEV 19 No such device
ENOTDIR 20 Not a directory
EISDIR 21 Is a directory
EINVAL 22 Invalid argument
ENFILE 23 File table overflow
EMFILE 24 Too many open files
ENOTTY 25 Not a typewriter
ETXTBSY 26 Text file busy
EFBIG 27 File too large
ENOSPC 28 No space left on device
ESPIPE 29 Illegal seek
EROFS 30 Read-only file system
EMLINK 31 Too many links
EPIPE 32 Broken pipe
EDOM 33 Math argument out of domain of func
ERANGE 34 Math result not representable

在 「/usr/include/asm-generic/errno.h」 中,对剩余的 errno 做了宏定义:

define errno explain
EDEADLK 35 Resource deadlock would occur
ENAMETOOLONG 36 File name too long
ENOLCK 37 No record locks available
ENOSYS 38 Function not implemented
ENOTEMPTY 39 Directory not empty
ELOOP 40 Too many symbolic links encountered
EWOULDBLOCK EAGAIN Operation would block
ENOMSG 42 No message of desired type
EIDRM 43 Identifier removed
ECHRNG 44 Channel number out of range
EL2NSYNC 45 Level 2 not synchronized
EL3HLT 46 Level 3 halted
EL3RST 47 Level 3 reset
ELNRNG 48 Link number out of range
EUNATCH 49 Protocol driver not attached
ENOCSI 50 No CSI structure available
EL2HLT 51 Level 2 halted
EBADE 52 Invalid exchange
EBADR 53 Invalid request descriptor
EXFULL 54 Exchange full
ENOANO 55 No anode
EBADRQC 56 Invalid request code
EBADSLT 57 Invalid slot
EDEADLOCK EDEADLK
EBFONT 59 Bad font file format
ENOSTR 60 Device not a stream
ENODATA 61 No data available
ETIME 62 Timer expired
ENOSR 63 Out of streams resources
ENONET 64 Machine is not on the network
ENOPKG 65 Package not installed
EREMOTE 66 Object is remote
ENOLINK 67 Link has been severed
EADV 68 Advertise error
ESRMNT 69 Srmount error
ECOMM 70 Communication error on send
EPROTO 71 Protocol error
EMULTIHOP 72 Multihop attempted
EDOTDOT 73 RFS specific error
EBADMSG 74 Not a data message
EOVERFLOW 75 Value too large for defined data type
ENOTUNIQ 76 Name not unique on network
EBADFD 77 File descriptor in bad state
EREMCHG 78 Remote address changed
ELIBACC 79 Can not access a needed shared library
ELIBBAD 80 Accessing a corrupted shared library
ELIBSCN 81 .lib section in a.out corrupted
ELIBMAX 82 Attempting to link in too many shared libraries
ELIBEXEC 83 Cannot exec a shared library directly
EILSEQ 84 Illegal byte sequence
ERESTART 85 Interrupted system call should be restarted
ESTRPIPE 86 Streams pipe error
EUSERS 87 Too many users
ENOTSOCK 88 Socket operation on non-socket
EDESTADDRREQ 89 Destination address required
EMSGSIZE 90 Message too long
EPROTOTYPE 91 Protocol wrong type for socket
ENOPROTOOPT 92 Protocol not available
EPROTONOSUPPORT 93 Protocol not supported
ESOCKTNOSUPPORT 94 Socket type not supported
EOPNOTSUPP 95 Operation not supported on transport endpoint
EPFNOSUPPORT 96 Protocol family not supported
EAFNOSUPPORT 97 Address family not supported by protocol
EADDRINUSE 98 Address already in use
EADDRNOTAVAIL 99 Cannot assign requested address
ENETDOWN 100 Network is down
ENETUNREACH 101 Network is unreachable
ENETRESET 102 Network dropped connection because of reset
ECONNABORTED 103 Software caused connection abort
ECONNRESET 104 Connection reset by peer
ENOBUFS 105 No buffer space available
EISCONN 106 Transport endpoint is already connected
ENOTCONN 107 Transport endpoint is not connected
ESHUTDOWN 108 Cannot send after transport endpoint shutdown
ETOOMANYREFS 109 Too many references: cannot splice
ETIMEDOUT 110 Connection timed out
ECONNREFUSED 111 Connection refused
EHOSTDOWN 112 Host is down
EHOSTUNREACH 113 No route to host
EALREADY 114 Operation already in progress
EINPROGRESS 115 Operation now in progress
ESTALE 116 Stale file handle
EUCLEAN 117 Structure needs cleaning
ENOTNAM 118 Not a XENIX named type file
ENAVAIL 119 No XENIX semaphores available
EISNAM 120 Is a named type file
EREMOTEIO 121 Remote I/O error
EDQUOT 122 Quota exceeded
ENOMEDIUM 123 No medium found
EMEDIUMTYPE 124 Wrong medium type
ECANCELED 125 Operation Canceled
ENOKEY 126 Required key not available
EKEYEXPIRED 127 Key has expired
EKEYREVOKED 128 Key has been revoked
EKEYREJECTED 129 Key was rejected by service
EOWNERDEAD 130 Owner died
ENOTRECOVERABLE 131 State not recoverable
ERFKILL 132 Operation not possible due to RF-kill
EHWPOISON 133 Memory page has hardware error

字符流/文本流/字节流/文件属性/目录/设备文件

基本文件I/O system calls

概述

在开始基本I/O 函数分享前还是说说Linux中文件描述符的管理方式吧;

image-sonf.png

image-oydp.png

  • 内核会为每个进程维护一个已打开文件文件表(filetable)。文件表是由一些非负整数进行索引,这些非负整数称为文件描述符(filedescriptors,简称fds)。
  • 列表的每一项是一个已打开文件的inode结构体地址。文件描述符使用C语言的int类型表示。这种表示方式正是继承了UNIX传统每个Linux进程可打开的文件数是有上限的。文件描述符的范围从0开始,到上限值减1。默认上限值为1024,但是可以对它进行配置,最大为1048576。因为负数不是合法的文件描述符,函数出错不能返回有效的fd时,通常常会返回-1
  • 每个进程默认打开三个文件分别是:STDIN FILENO, STDOUT_FILENO 和STDERR_FILENO
常见Linux C 头文件
  1. sys/types.h
    • 包含各种基本数据类型的定义,如 size_tpid_t
    • 通常用于定义与系统相关的数据类型。
  2. sys/stat.h: --- file status
    • 包含文件状态信息的结构体定义,如 struct stat
    • 提供了文件权限位的宏定义,如 S_IRUSRS_IWGRP
    • 用于获取和设置文件的状态信息。
  3. fcntl.h:--- file operation conctrl
    • 定义了文件控制操作所需的常量,如 O_RDONLYO_WRONLY
    • 提供了 open 函数和一些文件控制操作函数的声明。
    • 用于打开、关闭、复制和其他文件操作。
  4. unistd.h
    • 提供了 POSIX 系统调用的声明,如 readwriteclose
    • 包含一些常用的符号常量,如 STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO
    • 用于实现对文件描述符的基本 I/O 操作。
  5. errno.h
    • 定义了 errno 全局变量,该变量在系统调用失败时被设置为指示错误的整数值。
    • 包含一些与错误码相关的宏定义,如 EACCESEINVAL
    • 用于检查和处理系统调用产生的错误。

open(...)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open (const char *pathname, int flags);
int open(const char *pathname,int flags, mode_t mode);

flags:

O_RDONLY:只读打开文件
O_WRONLY: 只写打开文件
O_RDWR: 读写方式打开文件
O_CREAT: 如果文件不存在,则创建文件
O_EXCL: 与 O_CREAT 一起使用,用于确保文件是新创建的。
如果文件已存在且 O_EXCL 标志被指定,则 open 会失败。
O_TRUNC: 如果文件存在且以写方式打开,则将文件截断为零长度
O_APPEND: 在文件末尾追加数据
O_NONBLOCK: 非阻塞模式。文件描述符将以非阻塞方式打开,读写操作不会阻塞进程
O_SYNC: 打开文件以同步写入方式。所有写入操作将立即写入物理介质

Tip:前三个方式互斥,但可以与后面的进行或运算

mode 参数用于指定新文件的权限,它是一个三位的八进制数字,每一位表示不同的权限。这个参数通常与 O_CREAT 标志一起使用,以确保新文件被创建时有适当的权限设置。

mode

S_IRUSR (0400): 用户(文件所有者)具有读权限
S_IWUSR (0200): 用户具有写权限。
S_IXUSR (0100): 用户具有执行权限。
S_IRGRP (0040): 组具有读权限。
S_IWGRP (0020): 组具有写权限。
S_IXGRP (0010): 组具有执行权限。
S_IROTH (0004): 其他用户具有读权限。
S_IWOTH (0002): 其他用户具有写权限。
S_IXOTH (0001): 其他用户具有执行权限。

这些权限位可以通过按位或运算组合在一起,以设置文件的权限。例如,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH 表示文件所有者具有读写权限,而组和其他用户只有读权限。

mode 参数中,这些权限位可以使用宏来表示,如 S_IRWXU 表示用户有读、写和执行权限。这有助于提高代码的可读性。

create(...)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat (const char *pathname, mode t mode);

creat 系统调用用于以只写方式打开文件,并在文件不存在时创建它。它相当于使用 open 调用,其中包括了 O_WRONLY | O_CREAT | O_TRUNC 标志。

read(...)
#include <unistd.h>
ssize_t read(int fd, void *buf, sizet count);

read(...) 系统调用有6中不同的返回情况见下

return case
= count 正常情况,读取期望长度
< count 读缓冲区数据有限,返回值即为这次读取缓冲区所有数据的长度。
或者中间被中断,提前返回。再次read(...) 可能会返回0
0 没有数据可读或读文本类型文件遇到EOF。
-1(非阻塞模式) errno == EINTR 被信号中断,可重新调用read(...)
-1(非阻塞模式) ermo == EAGAIN 没有数据可读,可做其他作业,稍后再次读取
-1 返回值非 EINTR or EAGAIN, 表示遇到一个更严重的错误,重新read(...)可能不会成功

正确read(...) 使用示例见下:

#include <unistd.h>
#include <stdio.h>

int main() {
    int fd;
    char buf[BUFSIZ];
    ssize_t nr;

    // ... 假设要读取的文件描述符为fd
    // 这里只是示例,实际应用中应根据具体情况获取文件描述符

    start:
    nr = read(fd, buf, BUFSIZ);
    if (nr == -1) {
        if (errno == EINTR) {
            // 若因信号中断,重新读取
            goto start;
        } else if (errno == EAGAIN) {
            // 遇到EAGAIN,表示暂时没有数据可读,稍后再尝试
            printf("EAGAIN encountered. Waiting and retrying...\n");
            sleep(1); // 暂停1秒后重新尝试
            goto start;
        } else {
            // 其他错误
            perror("read");
            return 1;
        }
    } else if (nr == 0) {
        // 到达文件末尾
        printf("End of file reached.\n");
    } else {
        // 成功读取数据,处理读取到的数据...
    }

    return 0;
}
write(...)
#include <unistd.h>
ssize_t write(int fildes, const void *buf, size_t nbyte);

当write(...) 返回时,说明内核已经把数据从提供的缓冲区拷贝到内核缓冲区中,【在未设置同步直写标志的模式下内核不保证数据已经写入到存储块中。】在后台,内核收集所有这样的 “脏” 缓冲区(即存储的数据比磁盘上的数据新),进行排序优化,然后把这些缓冲区写到磁盘上。

EINTR 阻塞和非阻塞情况说明:
return casse
= nbytes normal
< nbytes (socket 非阻塞) errno ==EAGAIN
缓冲区已满,只写入了部分数据。同read(...)类似,重新调用write(...)
< nbytes (local file) errno ==EFBIG
该文件超过文件系统文件大小限制/写请求被中断
errno == ENOSPC 该文件系统空间满(磁盘没空间)
0 如果errno 没有被这次调用后设置过,表示无异常,有可能写入长度就是0
-1 (socket 非阻塞) errno ==EAGAIN 缓冲去满一个字节也写不下
errno == EINTR 被信号中断,可重新调write(...)

Tip EINTR :
a、阻塞fd:被一个信号打断,但是需要强调的是,在信号打断前没有写入一个字节,才会返回-1,errno设定为EINTR。如果有写入,返回已经写入的字节数。(os:如果写入了部分数据依然返回-1,errno设定为EINTR,处理完中断后,由于不知道被打断时写到了什么地方,也就不知道该从哪一个地方继续写入。)

b、非阻塞fd:调用非阻塞write,即使write被信号打断,write会继续执行未完成的任务而不会去响应信号。因为在非阻塞调用中,没有任何理由阻止read或者wirte的执行。

write注意事项
  • 基本操作注意事项
    • 返回值判断:write 函数执行成功时,会返回写入的字节数;出错时,返回 -1,并设置 errno 值。
    • 部分写情况
      • 普通文件:对于普通文件,除非发生错误,write 操作保证会执行整个写请求。通常不需要执行循环写操作,但对于其他文件类型,如 socket,可能需要循环来保证写了所有请求的字节。
      • 错误处理:当 write 操作返回 -1 时,需要根据 errno 的值进行错误处理。常见的 errno 值包括 EBADF、EFAULT、EFBIG、EINVAL、EIO、ENOSPC、EPIPE 等。
  • 特殊模式注意事项
    • Append 模式:当以 Append 模式(参数设置 O_APPEND)打开文件描述符时,写操作从当前文件的末尾开始,保证了文件位置指针总是指向文件末尾,避免了多个写进程之间的竞争问题。
    • 非阻塞写:以非阻塞模式(参数设置 O_NONBLOCK)打开文件,当发起写操作时,系统调用 write 可能会返回 -1,并设置 errno 值为 EAGAIN。在这种情况下,请求可以稍后重新发起。
  • 数据一致性注意事项
    • 延迟写问题
      • 数据同步:write 调用执行非常快,数据被拷贝到缓冲区后,内核会在后台将缓冲区的数据写到磁盘上。但这可能导致数据在系统崩溃时没有写入磁盘,存在数据丢失的风险。
      • 同步操作:为了保证数据按时写入磁盘,可以使用 fsync、fdatasync 等函数进行同步操作。
  • 其他注意事项
    • 参数限制
      • count 值:如果 count 值大于 SSIZE_MAX,调用 write 的结果是未定义的。
      • 写入零字节:调用 write 时,如果 count 值为零,会立即返回,且返回值为 0。

正确write(...)见下:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main() {
    int fd;
    const char *data = "Hello, World!";
    ssize_t numBytesWritten;

    // 打开一个文件用于写入,如果文件不存在则创建
    fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    do {
        numBytesWritten = write(fd, data, strlen(data));
        if (numBytesWritten == -1) {
            if (errno == EINTR) {
                // 处理信号中断错误,继续尝试
                continue;
            } else if (errno == EAGAIN) {
                // 处理暂时无法写入的情况,例如缓冲区已满或I/O资源不可用
                // 可以选择等待一段时间后再次尝试,或者根据具体情况进行相应的处理
                sleep(1); 
                continue;
            } else {
                // 处理其他错误
                perror("write");
                break;
            }
        } else if (numBytesWritten < strlen(data)) {
            // 处理部分写入的情况
            perror("Partial write");
            break;
        }
    } while (numBytesWritten < strlen(data));

    // 关闭文件描述符
    if (close(fd) == -1) {
        perror("close");
        exit(1);
    }

    if (numBytesWritten == strlen(data)) {
        printf("成功写入 %ld 个字节到文件\n", (long)numBytesWritten);
    } else {
        // 可以根据具体需求进行错误处理,例如提示错误信息或采取其他恢复措施
        printf("写入失败\n");
    }

    return 0;
}