Linux系统编程:从文件I/O到目录遍历的实战指南
1. 项目概述:从文件到目录,Linux系统编程的基石
如果你刚开始接触Linux系统编程,或者已经写过一些操作文件的C代码,那你大概率是从open、read、write、close这几个函数入门的。这没错,它们是处理单个文件的利器。但当我们把视角拉高,从“处理一个文件”切换到“管理一个系统”时,你会发现,文件从来不是孤立存在的。它们被组织在目录树中,程序的运行环境由一系列目录和文件路径定义,而高效地遍历、监控、操作这些目录结构,才是构建复杂、健壮应用的关键。这就是“目录编程”和“标准文件编程”要解决的核心问题。
简单来说,标准文件编程教会你如何读写文件里的“内容”,而目录编程则让你能掌控文件所在的“位置”和“关系”。前者是点,后者是面。只懂前者,你写的程序可能笨拙而低效;两者结合,你才能写出真正理解并驾驭Linux文件系统的程序。无论是写一个需要递归扫描用户文档的备份工具,一个实时监控日志目录变化的守护进程,还是一个需要动态加载配置插件的服务端程序,都离不开对目录和文件路径的深度操作。接下来,我会结合十多年的踩坑经验,带你深入这两个领域,不仅讲清楚API怎么用,更重点剖析它们背后的设计哲学、性能陷阱以及那些手册上不会写的实战技巧。
2. 标准文件I/O编程深度解析
在深入目录之前,我们必须夯实基础。Linux下的标准文件I/O(Input/Output)主要围绕一组POSIX标准定义的函数展开,它们提供了对文件描述符(File Descriptor, fd)的直接操作。理解这套机制,是理解一切高级抽象(如C标准库的FILE*)的根基。
2.1 文件描述符:一切操作的起点
文件描述符是一个非负整数,它是内核为了高效管理被进程打开的文件所创建的一个索引。当进程调用open()或socket()等系统调用时,内核会创建一个内部数据结构(如struct file)来记录这次打开的所有信息(如文件偏移量、访问模式、inode指针等),然后返回一个fd作为这个结构在进程文件描述符表中的句柄。
注意:文件描述符0、1、2通常分别对应标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。这是shell和系统约定俗成的,你的程序在启动时自动继承了它们。
内核维护着每个进程的“文件描述符表”,而fd就是这个表的数组下标。当你对fd进行read或write时,内核通过这个下标快速找到对应的struct file,进而操作真正的文件。这种设计使得I/O操作非常高效。
2.2 核心系统调用详解与实战陷阱
2.2.1 open():不仅仅是打开文件
open()函数的原型是int open(const char *pathname, int flags, mode_t mode);。flags参数是精髓所在,它决定了打开的方式和行为。
关键标志位组合与场景:
O_RDONLY | O_CLOEXEC: 以只读方式打开,并设置“执行时关闭”(Close-on-exec)标志。这是创建子进程前打开资源(如配置文件)的最佳实践,可以防止文件描述符无意中泄漏给子进程。O_WRONLY | O_CREAT | O_TRUNC: 以只写方式打开,如果文件不存在则创建,如果存在则将其截断为0字节。这是典型的“覆盖写入”模式,常用于日志轮转或生成全新输出文件。O_WRONLY | O_CREAT | O_APPEND: 以只写方式打开,如果不存在则创建,写入总是在文件末尾追加。这是日志记录的黄金标准,可以保证即使多个进程同时写同一个日志文件,数据也不会被覆盖,但请注意,这不能保证日志行的原子性写入(需要额外的同步机制)。O_RDWR | O_SYNC: 以读写方式打开,并要求每次write都等待数据物理写入磁盘后才返回。这保证了数据的持久性,但性能代价极大,通常只用于数据库事务日志等对数据一致性要求极高的场景。
我踩过的坑:O_CREAT与mode参数mode参数仅在flags中包含O_CREAT或O_TMPFILE时才有效。它指定了新创建文件的权限(如0644)。这里最大的坑是“umask”——进程的文件模式创建屏蔽字。最终文件的权限是mode & ~umask。例如,你指定mode=0666(所有人可读写),但系统默认umask=0022,最终文件权限将是0644(所有者可读写,其他人只读)。如果你需要精确控制权限,必须在open前调用umask(0)临时清除屏蔽,并在open后恢复,但这有安全风险。更安全的做法是,先用期望的权限创建文件,后续如果需要修改,再显式调用chmod。
2.2.2 read()/write():并非所见即所得
ssize_t read(int fd, void *buf, size_t count);和ssize_t write(int fd, const void *buf, size_t count);看似简单,但返回值处理是新手最容易出错的地方。
read()的返回值详解:
> 0: 成功读取的字节数。这个数字可能小于你请求的count!对于普通文件,在到达文件末尾(EOF)前,通常能读满缓冲区。但对于管道、套接字、终端等设备,部分读取(Short Read)是常态。你的代码必须能处理这种情况,在循环中持续读取,直到返回0或出错。= 0: 表示到达文件末尾(EOF),没有更多数据可读。-1: 出错,错误码在errno中。需要特别注意的是EINTR(系统调用被信号中断),这不是一个致命错误。一个健壮的程序应该检查errno是否为EINTR,如果是,则重启read调用。
write()的返回值与“部分写入”:与read类似,write的返回值也可能小于请求写入的count,这被称为“部分写入”(Short Write)。对于磁盘文件,在现代Linux上较少见(除非磁盘满),但对于网络套接字、管道等,由于缓冲区限制,部分写入非常普遍。你的代码必须在循环中继续写入剩余数据,直到全部写完或出错。
下面是一个健壮的写入函数示例:
ssize_t write_all(int fd, const void *buf, size_t count) { size_t total_written = 0; const char *ptr = (const char *)buf; while (total_written < count) { ssize_t written = write(fd, ptr + total_written, count - total_written); if (written == -1) { if (errno == EINTR) { continue; // 被信号中断,重试 } else { return -1; // 其他错误 } } if (written == 0) { // 有些情况下write可能返回0,视同错误或特殊处理 // 通常我们将其视为错误,因为没写入任何数据 errno = EIO; return -1; } total_written += written; } return total_written; }2.2.3 lseek()、fsync()与文件偏移量管理
每个打开的文件描述符都有一个关联的“当前文件偏移量”,它决定了下一次read或write操作开始的位置。lseek()可以修改这个偏移量。
lseek(fd, 0, SEEK_SET): 移动到文件开头。lseek(fd, -10, SEEK_END): 移动到文件末尾前的10个字节。lseek(fd, 0, SEEK_CUR):获取当前偏移量,这是一个非常有用的技巧,可以用来获取文件大小(对于非空文件,先lseek到末尾,获取偏移量,再lseek回来),或者用于调试。
数据持久化的关键:fsync()与fdatasync()当你调用write()成功返回,只意味着数据从用户空间拷贝到了内核的页面缓存(Page Cache),并不保证数据已经写入了物理磁盘。如果此时系统崩溃,数据可能会丢失。fsync(int fd)系统调用会阻塞,直到与fd相关的所有数据和元数据(如修改时间)都写入磁盘。fdatasync(int fd)则只强制写入数据部分,不保证元数据立即落盘,性能稍好。对于关键数据(如数据库事务),必须在适当的时机调用它们。
2.3 高级I/O:非阻塞、多路复用与异步
当程序需要同时处理多个文件描述符(如网络服务器)时,阻塞I/O(默认行为)会导致性能瓶颈。这时就需要更高级的I/O模型。
- 非阻塞I/O(Non-blocking I/O): 通过
open()时设置O_NONBLOCK标志,或使用fcntl(fd, F_SETFL, flags | O_NONBLOCK)来设置。在此模式下,如果read没有数据可读,或write缓冲区已满,调用会立即返回-1,并设置errno为EAGAIN或EWOULDBLOCK,而不是阻塞等待。程序需要轮询(polling)这些fd,效率低下。 - I/O多路复用(I/O Multiplexing): 这是解决多fd并发的主流方案。核心系统调用是
select、poll和epoll。它们允许进程告诉内核:“帮我监视这一组文件描述符,当其中任何一个就绪(可读、可写或有异常)时再通知我”。这样进程就可以在一个线程中高效地管理成千上万的连接。其中epoll是Linux特有的高性能机制,特别适合连接数多的场景。 - 异步I/O(Asynchronous I/O, AIO): 由
aio_read、aio_write等函数提供。进程发起一个I/O请求后立即返回,内核在整个I/O操作完成(数据已传输到用户缓冲区)后再通知进程。这与epoll等“就绪通知”模型有本质区别。Linux原生AIO(libaio)对磁盘文件支持较好,但对网络套接字的支持 historically 不完善,需要仔细评估。
3. 目录编程:驾驭文件系统的组织结构
掌握了文件操作,我们终于可以抬头看路,研究文件和目录的组织结构了。目录在Linux中本质上也是一种特殊类型的文件,里面存储的是“目录项”(dirent),即文件名到inode编号的映射。
3.1 目录遍历的核心:opendir、readdir与rewinddir
遍历目录是目录编程中最常见的任务。标准库提供了<dirent.h>头文件和相关函数。
#include <dirent.h> DIR *opendir(const char *name); struct dirent *readdir(DIR *dirp); int closedir(DIR *dirp); void rewinddir(DIR *dirp);struct dirent的关键成员:
ino_t d_ino: 文件的inode编号。对于btrfs等支持快照的文件系统,跨子卷遍历时,这个值可能不是唯一的,需要注意。char d_name[]: 以空字符结尾的文件名。这是唯一保证可移植的字段。其他如d_type(文件类型)是BSD扩展,并非所有文件系统都支持(比如某些网络文件系统),使用前最好用#ifdef _DIRENT_HAVE_D_TYPE检查。
一个健壮的目录遍历框架:
#include <stdio.h> #include <dirent.h> #include <errno.h> #include <string.h> void list_dir(const char *path) { DIR *dir = opendir(path); if (dir == NULL) { perror("opendir failed"); return; } errno = 0; // readdir在到达目录尾和出错时都返回NULL,需用errno区分 struct dirent *entry; while ((entry = readdir(dir)) != NULL) { // 跳过 . 和 .. 目录 if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } // 构造完整路径(注意:这不是线程安全的) char full_path[PATH_MAX]; snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name); // 这里可以根据d_type(如果支持)或lstat(full_path)获取文件类型 printf("Found: %s\n", full_path); } // 循环结束后,检查是否是错误导致的退出 if (errno != 0) { perror("readdir failed"); } closedir(dir); }重要提示:
readdir()不是线程安全的。如果多个线程需要同时读取同一个目录流DIR*,必须使用互斥锁进行保护。或者,每个线程使用自己独立的opendir()。
3.2 递归遍历与树形结构处理
列出单层目录很简单,但实际需求往往是递归遍历整个子树,例如计算目录总大小、查找特定文件、批量重命名等。递归是实现的最直观方式,但需要注意几个关键点:
- 符号链接循环:目录树中可能存在通过符号链接形成的循环(A目录包含指向B目录的链接,B目录又包含指向A的链接)。一个不处理循环的递归程序会陷入无限循环直到栈溢出。解决方案是记录已访问目录的
dev和ino(通过stat()获取),在进入子目录前检查是否已访问过。 - 路径名长度限制:
PATH_MAX(通常为4096)是系统定义的路径名最大长度限制。在深度递归时,构造的完整路径名可能超过此限制。更安全的方法是使用*at系列函数(如openat、fstatat),它们使用目录文件描述符(dirfd)和相对路径,避免构造长绝对路径。 - 内存与性能:对于超大型目录(如包含数百万文件的目录),递归可能导致栈空间紧张或性能下降。可以考虑使用显式栈(非递归深度优先搜索)或广度优先搜索(BFS)来管理遍历状态。
3.3 目录的创建、删除与权限管理
mkdir(const char *pathname, mode_t mode): 创建目录。同样受umask影响。一个常见需求是创建多级目录(如/a/b/c),mkdir本身只创建最后一级(c),如果父目录不存在会失败。你需要自己实现递归创建,或者使用mkdir -p命令对应的库函数mkdirp(非标准,需自己实现或使用第三方库)。rmdir(const char *pathname): 删除一个空目录。如果目录非空,会失败并设置errno为ENOTEMPTY。要删除非空目录树,需要先递归删除其所有子项,这通常需要自己编写递归函数或使用nftw()(文件树遍历)函数。chdir(const char *path): 改变进程的当前工作目录。这是一个影响整个进程状态的系统调用。在多线程程序中要极其小心,因为工作目录是进程级共享的,一个线程改变目录会影响所有其他线程。通常建议使用绝对路径或*at系列函数来避免依赖和改变工作目录。getcwd(char *buf, size_t size): 获取当前工作目录的绝对路径。确保buf足够大(至少PATH_MAX字节),否则可能返回NULL并设置errno为ERANGE。
4. 文件元数据与属性操作实战
文件不仅仅是数据块,还有大量的元数据(Metadata),如权限、所有者、大小、时间戳等。操作这些属性是系统编程的日常。
4.1 stat家族:获取文件状态的权威接口
int stat(const char *pathname, struct stat *statbuf);是最常用的函数。它通过路径名获取文件信息,并填充到struct stat结构中。这里有几个重要的变体:
int fstat(int fd, struct stat *statbuf);: 通过已打开的文件描述符获取信息。当你已经有一个fd时(比如从open或socket获得),用这个更高效,因为它避免了路径解析。int lstat(const char *pathname, struct stat *statbuf);:关键区别!对于符号链接(symlink),stat会追踪(follow)链接,返回链接指向的目标文件的信息;而lstat不追踪,返回符号链接本身的信息。在遍历目录树时,如果你需要区分符号链接和真实文件/目录,必须使用lstat。
struct stat中的宝藏:
st_mode: 一个位掩码,包含文件类型(S_ISREG(m),S_ISDIR(m)等宏判断)和权限位(S_IRUSR,S_IWGRP等)。st_ino,st_dev: inode编号和设备ID。两者组合可以唯一标识一个文件系统内的文件。用于检测硬链接或防止递归遍历中的循环。st_size: 对于普通文件,是字节大小;对于符号链接,是链接路径的字符串长度;对于目录,其意义是文件系统相关的(通常是一个块大小)。st_mtime,st_ctime,st_atime: 修改时间、状态改变时间、访问时间。注意,很多系统为了性能默认使用相对宽松的atime更新策略,如果需要精确的访问时间,可能需要挂载文件系统时使用strictatime选项。
4.2 权限修改:chmod、chown与umask
int chmod(const char *pathname, mode_t mode);/int fchmod(int fd, mode_t mode);: 修改文件权限。注意,只有文件所有者和超级用户才能执行此操作。int chown(const char *pathname, uid_t owner, gid_t group);: 修改文件所有者和所属组。同样有fchown变体。这个操作权限要求很高,通常只有root用户能随意更改。mode_t umask(mode_t mask);: 设置进程的文件创建屏蔽字,并返回之前的值。它是一个进程级属性,影响所有后续由该进程创建的文件的权限。通常只在程序启动时设置一次。
4.3 链接与重命名:文件系统的“指针”操作
- 硬链接(Hard Link):
int link(const char *oldpath, const char *newpath);。在文件系统中创建一个新的目录项(newpath),指向与oldpath相同的inode。硬链接与原始文件完全平等,无法区分谁是“原始”的。硬链接不能跨文件系统(因为inode编号是文件系统内的),也不能指向目录。删除一个硬链接只是减少inode的链接计数,当计数为0时,inode和数据块才会被真正释放。 - 符号链接(Soft/Symbolic Link):
int symlink(const char *target, const char *linkpath);。创建一个特殊的小文件(linkpath),其内容是指向target的路径字符串。它可以跨文件系统,可以指向目录,甚至可以指向一个不存在的目标。删除符号链接只删除这个链接文件本身,不影响目标。使用readlink()可以读取链接的内容。 - 重命名/移动:
int rename(const char *oldpath, const char *newpath);。这是一个原子操作。如果newpath已存在且不是一个目录,它会被原子地替换。如果oldpath和newpath在同一个文件系统内,这通常只是一个目录项信息的更新,非常快速。如果跨文件系统,则可能涉及拷贝和删除。重要特性:rename是少数几个能保证在系统崩溃时不会导致数据处于半完成状态的操作之一(假设文件系统支持日志),常用于实现“原子写入”:先写入一个临时文件,然后rename临时文件为目标文件名。
5. 实战案例:构建一个简易的目录树统计工具
理论说得再多,不如动手写一个。我们来设计一个工具dirstat,它递归遍历指定目录,统计文件数量、目录数量、总文件大小,并可以按扩展名筛选。
5.1 设计思路与数据结构
我们需要一个递归函数process_entry,它接收一个目录的路径(或文件描述符)和一个代表统计结果的结构体指针。结构体设计如下:
typedef struct { unsigned long long file_count; unsigned long long dir_count; unsigned long long total_size; // 字节 const char *filter_ext; // 例如 ".c",为NULL则不过滤 } Stats;为了避免符号链接循环,我们使用一个“已访问集合”。但由于inode可能重复(跨文件系统或某些特殊场景),更可靠的是记录(st_dev, st_ino)对。我们可以使用一个简单的哈希表或集合库(如GLib的GHashTable),但为了简化,这里假设目录树没有循环,或使用一个限制递归深度的简单防护。
5.2 核心递归遍历实现
以下是核心递归函数的简化版,重点展示逻辑:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <dirent.h> #include <errno.h> #include <limits.h> // 假设的Stats结构 typedef struct { ... } Stats; static int process_directory(const char *dir_path, Stats *stats, int depth) { if (depth > 100) { // 简单的深度防护 fprintf(stderr, "Warning: Maximum recursion depth exceeded at %s\n", dir_path); return -1; } DIR *dir = opendir(dir_path); if (!dir) { perror("opendir failed"); return -1; } struct dirent *entry; errno = 0; while ((entry = readdir(dir)) != NULL) { // 跳过 . 和 .. if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } // 构造完整路径(生产环境应用openat等避免路径长度问题) char full_path[PATH_MAX]; if (snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name) >= sizeof(full_path)) { fprintf(stderr, "Path too long: %s/%s\n", dir_path, entry->d_name); continue; // 跳过这个过长的路径 } struct stat statbuf; // 使用lstat,因为我们不打算跟随符号链接进入(避免循环和统计偏差) if (lstat(full_path, &statbuf) == -1) { perror("lstat failed"); continue; } if (S_ISDIR(statbuf.st_mode)) { // 它是一个目录,递归处理 stats->dir_count++; if (process_directory(full_path, stats, depth + 1) != 0) { // 可选:记录错误但继续处理其他条目 } } else if (S_ISREG(statbuf.st_mode)) { // 它是一个普通文件 if (stats->filter_ext == NULL || (strlen(entry->d_name) > strlen(stats->filter_ext) && strcmp(entry->d_name + strlen(entry->d_name) - strlen(stats->filter_ext), stats->filter_ext) == 0)) { // 扩展名匹配或不过滤 stats->file_count++; stats->total_size += statbuf.st_size; } } else if (S_ISLNK(statbuf.st_mode)) { // 它是一个符号链接,根据需求决定是否统计或跟随 // 本例中,我们不统计链接本身,也不跟随 // printf("Symbolic link: %s\n", full_path); } // 可以继续处理其他类型:管道、套接字、设备文件等 } if (errno != 0) { perror("readdir error"); } closedir(dir); return 0; }5.3 性能优化与边界处理思考
上面的代码是一个教学示例,在实际产品中需要考虑更多:
- 性能:对于包含数十万文件的目录,每次
lstat都是一个系统调用,开销很大。如果只需要文件类型,可以优先使用dirent->d_type(如果可用),它是一个从目录项直接读取的宏,无需系统调用。但如前所述,其支持性不是100%。 - 内存与栈:深度递归可能爆栈。可以改用显式栈(自己维护一个待处理目录的栈或队列)来实现非递归遍历。
- 路径构造:使用
snprintf构造路径有PATH_MAX限制风险,且效率不高。在生产环境中,更推荐使用openat、fstatat系列函数,配合目录文件描述符进行相对路径操作。 - 错误恢复:当前代码遇到错误(如权限不足
EACCES)会跳过单个条目或整个目录。更健壮的做法可能是记录错误,尝试继续处理其他可访问的部分。 - 并发:可以使用多线程来并行处理不同的子树,但需要小心同步对共享
Stats结构的更新(使用原子操作或互斥锁),并注意readdir不是线程安全的问题(每个线程应用自己的DIR*)。
6. 常见问题、调试技巧与进阶方向
6.1 高频错误与排查清单
“Too many open files” (EMFILE)
- 原因:进程打开的文件描述符数量达到了系统或用户限制。
- 排查:使用
lsof -p <PID>查看进程打开了哪些文件。使用ulimit -n查看和修改shell的资源限制(只对当前shell启动的进程有效)。在程序中,可以用getrlimit(RLIMIT_NOFILE, ...)获取和setrlimit设置。 - 解决:确保及时
close()不再需要的文件描述符。对于需要打开大量文件的程序(如代理服务器),在启动时适当调高RLIMIT_NOFILE限制。
“Permission denied” (EACCES)
- 原因:对路径中的某个目录没有执行(
x)权限,导致无法进入;或对文件没有读(r)/写(w)权限。 - 排查:使用
strace命令跟踪程序系统调用,看具体在哪一步失败。检查路径中所有父目录的权限 (ls -ld /path/to/parent)。
- 原因:对路径中的某个目录没有执行(
“Is a directory” (EISDIR) / “Not a directory” (ENOTDIR)
- 原因:系统调用期望的参数类型与实际不符。例如,用
open()以O_WRONLY打开一个目录,或用rmdir()删除一个普通文件。 - 解决:在操作前,先用
lstat()或fstat()检查文件类型。
- 原因:系统调用期望的参数类型与实际不符。例如,用
“No such file or directory” (ENOENT)
- 常见场景:在
open()或stat()时,提供的路径名中某个中间目录不存在;或者在多线程/多进程中,文件在检查存在(access)和打开(open)之间被其他进程删除(TOCTTOU竞态条件)。 - 解决:对于TOCTTOU,最好的方法是让操作原子化。例如,不要先
access()再open(),而是直接open()并处理可能的错误。对于创建文件,使用O_CREAT | O_EXCL标志可以原子性地检测文件是否存在并创建。
- 常见场景:在
“File exists” (EEXIST)
- 原因:使用
open()带O_CREAT | O_EXCL标志,或mkdir()时,目标路径已存在。 - 场景:这常用于创建锁文件或确保唯一临时文件。
- 原因:使用
6.2 调试利器:strace与ltrace
strace:跟踪进程执行的系统调用和接收到的信号。对于文件/目录操作的问题,它是第一诊断工具。
strace -e trace=file,desc my_program arg1 arg2 # 只跟踪与文件、描述符相关的系统调用 strace -f my_program # 跟踪子进程 strace -p <PID> # 附加到正在运行的进程通过观察
openat、read、write、stat等调用的参数和返回值,可以清晰地看到程序与文件系统的交互过程,定位是哪个调用失败以及失败原因。ltrace:跟踪进程调用的库函数。如果你想看程序调用了哪些
libc函数(如opendir,readdir),可以使用它。但注意,strace看到的系统调用才是最终与内核交互的底层事实。
6.3 进阶学习方向
掌握了这些基础后,你可以向更深处探索:
- inotify / fanotify:用于监控文件系统事件的机制。
inotify可以监视单个文件或目录的创建、修改、删除、移动等事件,是实现文件同步、热重载配置等功能的核心。fanotify功能更强大,可以监控整个挂载点,并能拦截访问请求,常用于防病毒软件或完整性监控。 - 文件系统链接与挂载:深入理解硬链接、符号链接、挂载点(mount point)、绑定挂载(bind mount)的区别和相互作用。理解
stat()中的st_dev字段如何标识不同的文件系统。 - /proc 与 /sys 文件系统:这两个虚拟文件系统是Linux内核暴露给用户空间的接口。通过读写
/proc/<pid>/下的文件,可以查询和修改进程状态;通过/sys可以操作内核参数和设备。它们也是“一切皆文件”哲学的极致体现。 - 异步I/O与io_uring:Linux内核最新的高性能异步I/O接口
io_uring,相比传统的libaio,它通过共享内存环队列极大地减少了系统调用开销,能够提供极高的I/O性能,是现代高性能服务器(如Nginx, Redis新版本)的底层支撑技术之一。 - 文件锁:
fcntl()提供的建议性锁(Advisory Lock)和flock(),用于协调多个进程对同一文件的访问,防止数据损坏。但要注意它们是“建议性”的,所有进程必须遵守锁协议才有效。
