Linux文件描述符与IO:从open到fsync的底层原理实践
发布时间:2026/9/8 5:20:06 锦皓数字建站

1. 文件描述符理解IO的第一块基石1.1 一切皆文件那么“文件”到底是什么Linux系统编程里有一句被说烂了的话一切皆文件。但落实到代码层这句话到底是什么意思简单说你操作键盘、屏幕、网络socket、磁盘上的普通文件用的都是同一套接口open、read、write、close。而把这些东西统一起来的核心抽象就是文件描述符。让我用一个生活化的类比来拆解文件描述符文件描述符就是一个整数它是进程打开文件的“索引号”。你可以把它想象成图书馆的索书号。图书馆里有成千上万本书文件管理员内核给每本借出去的书发一个编号fd你凭这个编号去取书、还书。进程每次打开一个文件内核就分配一个整数编号给这个进程之后这个进程所有对该文件的操作都通过这个编号来指定。这里有几个关键点必须搞清楚文件描述符是进程级的概念。每个进程都有自己独立的fd表进程A的fd 3和进程B的fd 3没有任何关系它们可能指向完全不同的文件。fd的分配规则是最小未使用原则。比如0、1、2被标准输入、标准输出、标准错误占用那么你第一次open一个文件返回的fd就是3。fd本身是int类型所以它很小通常0到1023是有效的受进程的RLIMIT_NOFILE限制超出范围的操作会直接报EBADF。我自己在带新人时经常发现很多人会把fd和文件指针FILE搞混。fd是内核层面的句柄是整数通过open/read/write使用FILE是标准C库的封装是结构体指针内部包含了fd但额外包了一层用户态缓冲区。这个区别在后文讲缓冲的时候会非常重要现在先记住fd是内核的通行证FILE*是用户态的便利贴。1.2 进程打开的文件远不止磁盘文件很多教材讲基础IO举的例子永远是磁盘上的txt文件这其实会误导初学者。实际工作中fd对应的对象五花八门普通文件磁盘上的txt、bin目录目录本身也是文件open一个目录也能拿到fd但只能read目录项不能write字符设备/dev/tty、串口块设备SD卡、SSDsocket网络通信管道pipe匿名内存映射文件shm_open这就解释了为什么系统编程里说“一切皆文件”。所以基础IO学完后你会发现网络编程、IPC、并发编程那些章节本质上还是围绕文件描述符在转。我个人的经验是把基础IO学透了后面的章节至少轻松一半。2. 核心API逐个过open、write、read、lseek、close2.1 open一个接口背后是一个决策表open是IO的起点它的原型长这样#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_CREAT时启用第三个参数mode指定新文件的权限位。新手最容易在这两个原型之间犯迷糊记住一条铁律只要flags里没有O_CREATmode参数就别传传了也没意义还容易让代码变得难读。flags本身是一个按位或的组合常用的有O_RDONLY0只读打开。注意这个值实际是0所以它和O_WRONLY1、O_RDWR2不是独立的位而是三个互斥的取值。你绝不能写O_RDONLY | O_WRONLY这种代码那是脏代码。O_CREAT文件不存在就创建。如果文件已存在这个标志没有额外动作相当于“打开或创建”。O_EXCL必须和O_CREAT一起用。如果文件已存在open直接失败返回-1。这个组合常用来做“互斥创建”比如守护进程的锁文件。O_TRUNC打开时把文件长度截断为0。这个标志很危险配合O_RDONLY | O_TRUNC会把文件清空即使你不是写打开。我见过有人用O_RDONLY读一个重要日志文件结果因为多写了个O_TRUNC把日志全清了。O_APPEND每次写都追加到文件末尾。这个标志的实现是原子的也就是说即使多进程同时写也不会互相覆盖内核会保证每次write的偏移量都在文件末尾。O_NONBLOCK非阻塞模式。对普通文件没有效果对设备文件、管道、socket等有意义。另一个容易被忽略的参数是mode的权限位。比如int fd open(data.txt, O_CREAT | O_WRONLY, 0644);这里的0644就是八进制表示的权限位owner可读可写6group可读4others可读4。实际生效的权限还要经过umask过滤。进程的umask默认一般是0022那么0644 ~0022 0644 0755 0644所以创建出来的文件权限就是0644。如果你的mode写0666但umask是0022实际创建出来是0644这点经常让新手困惑——你写的不一定是你得到的这还是umask的锅。2.2 write一个“部分写入”的陷阱write的原型#include unistd.h ssize_t write(int fd, const void *buf, size_t count);参数一眼就能看懂fd是目标文件buf是要写的数据count是要写的字节数。返回值是实际写入了多少字节返回-1表示出错。这里最容易被忽略的坑是write并不保证一次写完count个字节。对于普通磁盘文件大多数情况下write会一次性写完你给的数据但你不能依赖这个“大多数”。在网络socket、管道、终端设备上部分写入是常态。比如你写一个4KB的数据块到socket对方接收缓冲可能只有1KB你的write可能只写入了1KB然后返回剩下的3KB需要你在循环里继续写。这就是“部分写入”问题。标准做法是用一个循环包裹writessize_t writen(int fd, const void *buf, size_t count) { size_t written 0; while (written count) { ssize_t ret write(fd, (char*)buf written, count - written); if (ret 0) { if (errno EINTR) continue; // 被信号打断重试 return -1; } written ret; } return written; }这段代码里有一个额外的细节值得注意EINTR错误。当进程在write调用中被信号打断时write会返回-1并且errno被设置为EINTR。很多初学者看到EOF就直接报错退出实际上对于EINTR正确的做法是重试。我的习惯是只要write返回-1先检查errno如果是EINTR就继续其他错误再抛异常。2.3 readEOF和错误是两个不同的概念read的原型ssize_t read(int fd, void *buf, size_t count);它从fd中读取最多count个字节到buf返回实际读取的字节数。这里有三类返回值返回值 0读取到了一些字节。返回值 0遇到了文件结束EOF没有更多数据可读。返回值 -1出错需要用errno判断具体原因。注意read的返回值是可能小于count的。比如从socket读取对方可能只发来几十个字节你的buf有4096字节read可能返回50。对于read最安全的处理是每次都根据返回值而不是期望一次读满。这就是为什么很多网络IO框架里要维护一个“读缓冲”把没读满的字节先保存等凑齐一个完整消息再处理。在文件末尾处read返回0但返回0并不意味着出错。我自己调试时见过太多人把read返回0当成异常退出结果文件明明正常读完了程序却报错退出排查半天才发现判断条件写错了。2.4 lseek文件偏移量是个什么状态lseek的原型#include sys/types.h #include unistd.h off_t lseek(int fd, off_t offset, int whence);它设置文件偏移量返回新的偏移量。三个whence取值SEEK_SET偏移量从文件开头计算offset通常是正数。SEEK_CUR偏移量从当前位置计算offset可正可负。SEEK_END偏移量从文件末尾计算offset通常是负数。注意lseek有几个特殊行为它不进行任何IO操作只是修改了文件描述符表中的偏移量字段所以它是廉价的。它可以设置偏移量到文件末尾之后的位置这就是“文件空洞”。比如你在文件开头写一个字节然后seek到100MB的位置再写一个字节那么文件大小就是100MB1但中间的空间在磁盘上并未实际分配取决于文件系统。读取空洞区域时读到的是全零字节。对于管道、socket、终端设备lseek会失败返回-1errno为ESPIPE。所以网络编程里你根本不需要lseek。还有一个细节O_APPEND标志和lseek是互相制衡的。如果你打开了O_APPEND无论lseek把偏移量设置到哪里每次write都会自动移动偏移量到文件末尾再写入。这既是保证多进程安全追加的功能也让那些想用lseekwrite实现随机读写的程序在O_APPEND下失效。2.5 close不关闭的代价是什么close的原型int close(int fd);它释放文件描述符并让该fd可以被后续的open复用。close的返回值不是重点重要的是你是否养成了关闭的习惯。如果进程不关fd会有什么后果fd表是有限的默认一般是1024个。如果程序反复打开文件而不关闭最终会耗尽fd表后续open操作全部失败报“Too many open files”EMFILE。这种问题在长时间运行的服务里尤其致命。我遇到过线上服务跑了一天后突然报“open: Too many open files”排查后发现是一个循环里忘了关fd文件描述符泄漏了。所以我的习惯是open之后立刻在代码里规划好close位置最好用RAII自动管理。C语言没有RAII那就手动保证每个错误分支都关闭fd。3. 缓冲区用户态缓冲与内核态缓冲的层级关系3.1 你写的字节并没有立刻落盘写一个经典的实验程序#include fcntl.h #include unistd.h int main(void) { int fd open(test.txt, O_CREAT | O_WRONLY | O_TRUNC, 0644); write(fd, hello, 5); // 不调用close直接sleep sleep(10); return 0; }在这个程序sleep的10秒里你打开另一个终端执行cat test.txt会看到一个空文件。这是因为write仅仅把数据从用户态缓冲区拷贝到了内核页缓存page cache还没有真正刷到磁盘设备上。数据什么时候才会落盘取决于内核的pdflush/回写线程的策略、缓冲区满、或者你主动调用fsync。3.2 标准IO库的三级缓冲如果你用C标准库的fwrite而不是write还有一个额外的用户态缓冲层。标准IO库stdio会把小片段聚合成大块再一次性调用write写入内核。这个机制极大地减少了系统调用次数提升了性能。stdio的缓冲模式有三种全缓冲_IOFBF缓冲区满了通常是4096字节或8KB才写入。这是普通磁盘文件的默认模式。行缓冲_IOLBF遇到换行符就写入。这是终端的默认模式。所以你在终端上printf(hello\n)会立刻显示但printf(hello)没加换行可能等缓冲区满了才显示。无缓冲_IONBF每次fwrite都直接调用write。stderr默认就是无缓冲模式所以错误信息总是立刻输出。可以用setvbuf函数来修改缓冲模式。这解释了一个经典问题为什么fork之后子进程里printf(hello)交替执行会看到输出混乱甚至输出重复因为stdio缓冲区里的内容被子进程复制了一份子进程退出时flush缓冲区导致内容写了两遍。这个坑在“daemon化”和“fork多进程”的代码里非常常见。3.3 fsync、fdatasync、sync三者的区别如果你要确保数据落盘必须调用同步接口#include unistd.h int fsync(int fd); // 将fd对应的文件数据和元数据同步写入磁盘 int fdatasync(int fd); // 只同步数据块不同步元数据如文件大小、修改时间性能略高 void sync(void); // 将所有脏缓存都写入磁盘全局操作代价高fsync会阻塞到数据真正落盘才返回。注意fsync只对指定的单个文件有效。如果你写了一个文件的多个副本每个都要调用fsync。fdatasync则更激进它避免同步元数据因为元数据同步通常需要额外的磁盘寻道时间性能更好。这个接口在数据库、日志系统里是保命利器。写WAL预写日志时如果fsync不严格宕机后数据可能丢失或损坏。我见过一个存储项目上线初期因为忽略了fsync断电测试时丢了不少数据。从那以后我们所有关键写路径上都有fsync。3.4 exit与_exit缓冲区被谁抛弃这个坑非常经典。看代码#include stdio.h #include unistd.h #include stdlib.h int main(void) { printf(before exit\n); _exit(0); }运行结果是什么什么也不输出。原因printf把数据放到了stdio的用户态缓冲区程序直接调用_exit时不会刷新stdio缓冲区缓冲就在进程退出时被丢弃了。而exit会先调用stdio的清理函数包括刷新缓冲区再进入内核退出流程。理解这个机制对排查“看不到日志”“输出延迟”这类问题特别关键。如果你往管道或文件里打印日志但没加换行日志会攒在缓冲区里进程意外退出比如被信号杀死时日志就丢了。一个万无一失的做法是写日志要么加换行要么在关键路径上主动fflush或fsync。4. fd的复制与重定向dup、dup2和shell重定向的底层原理4.1 dup复制出一个最小可用fd#include unistd.h int dup(int oldfd); int dup2(int oldfd, int newfd); int dup3(int oldfd, int newfd, int flags); // Linux特有dup返回一个未使用的最小fd它指向同一个文件表项。dup2则把newfd设置为oldfd的副本如果newfd已经打开它会被先静默关闭然后把newfd指向oldfd所指向的文件表项。为什么需要复制fd一个经典应用场景是实现shell的输入输出重定向。执行cmd out.txt时shell会open(out.txt, O_CREAT | O_WRONLY | O_TRUNC, 0644)得到一个fd比如是3。调用dup2(3, 1)把stdoutfd 1重定向到刚刚打开的fd 3。close(3)。然后exec执行cmdcmd的stdout自动写入了out.txt。这套流程是不是很熟这就是shell重定向的底层原理。所以你在shell里执行echo hello out.txt本质上就是dup2在背后帮你操作了fd数组。同理21也是在fd层做复制把fd 2指向fd 1指向的位置。如果没有dup2你手动用close(1)再open(out.txt)也能达到类似效果但有个问题open返回的fd不一定正好是1。close(1)之后立即open根据最小fd分配原则open大概率返回1但如果你在多线程环境里另一个线程在这间隙打开了新文件就可能抢走fd 1。而dup2的复制是原子的它内部处理了“目标fd已被占用”的情况整个操作不可被其他线程插入这就是它写shell重定向更安全的原因。4.2 文件描述符表和文件表项的关系要真正理解dup/dup2需要知道内核里fd到底存了什么。进程内部维护三张表文件描述符表每进程一张每个条目指向文件表项。每个fd有独立的标志close-on-exec标志就是存在这里的。文件表项维护文件的偏移量、打开模式O_APPEND等、文件状态标志。每个文件表项有自己的refcount。多个fd可以指向同一个文件表项比如dup之后oldfd和newfd指向同一个文件表项偏移量是共享的。inode表记录文件在磁盘上的元数据和数据块位置。这意味着dup复制出来的fd不是复制文件偏移量而是共享同一个文件表项。所以用dup之后oldfd和newfd对同一文件的写是共享偏移量的。如果你开了O_APPEND两个fd写的内容不会互相覆盖。但如果你在不同的fd上各自lseek到不同位置再写就会交叉错乱。4.3 一个用dup2实现标准输出重定向的例子来看一段完整的demo#include fcntl.h #include unistd.h #include stdio.h #include stdlib.h int main(void) { int fd open(output.log, O_CREAT | O_WRONLY | O_TRUNC, 0644); if (fd 0) { perror(open); exit(1); } // 把stdout重定向到文件 if (dup2(fd, STDOUT_FILENO) 0) { perror(dup2); exit(1); } close(fd); // 现在printf的输出会写到output.log printf(Hello, redirected!\n); return 0; }注意运行前先确认你的程序会不会干扰其他输出。重定向stdout之后所有printf都进文件了终端上什么也看不到。如果你在维护脚本时用到类似重定向一定要记住重定向是进程级的影响当前进程所有的标准输出。这也解释了很多守护进程把日志重定向到文件的默认做法。4.4 close-on-exec标志一个隐藏的守护者继续聊fd的一个隐藏属性FD_CLOEXEC。这个标志表示在执行exec系列函数时自动关闭这个fd。为什么需要它如果不设置exec后子进程会继承所有fd包括那些本不该暴露给新程序的fd比如数据库连接、监听socket。这是安全漏洞的经典来源。想象一下你forkexec一个外部程序这个程序只要能猜到你监听的fd就可以通过这个fd读写你的连接。正确的做法是open时带上O_CLOEXEC标志int fd open(secret.txt, O_RDONLY | O_CLOEXEC);或者用fcntl设置已有的fdint flags fcntl(fd, F_GETFD); flags | FD_CLOEXEC; fcntl(fd, F_SETFD, flags);如果你写的是库代码被别人调用你没有O_CLOEXEC那么这个fd会在exec时泄漏给子进程这是非常危险的。所以我的原则是所有open都默认加O_CLOEXEC除非你明确知道需要跨exec保留。5. 标准IO与系统调用的取舍什么时候用哪个5.1 性能差异系统调用没那么便宜很多初学者觉得“标准IO效率高因为缓冲”这句话只对了一半。stdio确实通过用户态缓冲减少了系统调用次数但你要注意它和write/read这种直接系统调用的适用场景。系统调用syscall是进出内核的过程涉及模式切换、寄存器保存/恢复、内核态栈切换开销比普通函数调用大得多。一次read/write或者printf内部最终也会调用write所以整个链路的开销大头在系统调用本身。stdiio把多次printf聚合成一次write明显减少了系统调用次数所以用fwrite确实比用write自己拼缓冲快。但反过来说stdio缓冲意味着延迟数据在用户态缓冲里积压你无法立即知道是否已经进入内核。如果你需要实时写入机制比如写日志后要立刻立即可见给其他进程读用系统调用更直接。或者你也可以用setvbuf调整stdio的缓冲模式甚至设置无缓冲。5.2 控制粒度系统调用的精细度标准IO库提供了fseek、fprintf、fscanf、fgets这类方便函数但封装的代价是你失去了对文件偏移量、原子性、并发安全的精细控制。举几个实际例子你想要“原子追加写”只能通过open的O_APPEND实现。stdio的fprintf并不保证多进程间写不交叉。你想要lseek到文件中间写几个字节OS的write是原子的吗不一定。多进程同时写同一个文件如果不加锁内容就会错乱。系统调用层面的write对普通文件不保证原子性除非写的是管道且PIPE_BUF内。你想知道具体写入多少字节stdio的fwrite返回的是“成功写入多少元素”而不是字节数。你想控制写盘时机只有系统级别的fsync/fdatasync能做到。stdio层面没有回调钩子。所以我的经验是需要落盘可靠性的代码用系统调用层需要大量字符串格式化输出的先用stdio拼好再一次性用write写出。两种方式并不互斥可以组合使用。5.3 一个完整示例用read/write实现cp命令这里给一个纯系统调用的cp实现它比标准库版本更接近内核#include fcntl.h #include unistd.h #include stdio.h #include stdlib.h #include sys/stat.h #define BUFSIZE 4096 int main(int argc, char *argv[]) { if (argc ! 3) { fprintf(stderr, Usage: %s src dest\n, argv[0]); exit(1); } int src open(argv[1], O_RDONLY); if (src 0) { perror(open src); exit(1); } int dst open(argv[2], O_CREAT | O_WRONLY | O_TRUNC, 0644); if (dst 0) { perror(open dst); close(src); exit(1); } char buf[BUFSIZE]; ssize_t n; while ((n read(src, buf, BUFSIZE)) 0) { ssize_t written 0; while (written n) { ssize_t ret write(dst, buf written, n - written); if (ret 0) { perror(write); close(src); close(dst); exit(1); } written ret; } } if (n 0) { perror(read); close(src); close(dst); exit(1); } close(src); close(dst); return 0; }这段代码有几个细节值得注意缓冲区用了4KB这是很多文件系统的块大小一次read能填满一个块减少系统调用的次数。外层的read循环内层的write循环处理部分写入。每次系统调用都检查返回值出错就及时关闭已打开的fd。这叫“错误路径清理”是写长时间运行服务的必备素养。5.4 大文件拷贝的性能实验我有一次对比了三种拷贝方式用read/write按每次1字节拷速度惨不忍睹约几MB/s。用read/write按每次64KB拷速度接近1GB/s。用mmapmemcpy速度相差不大但开启页面缓存后甚至略慢于64KB的read/write。这说明了什么系统调用次数的开销确实是性能瓶颈但更大的块也能减少开销。64KB是很多测试里read/write的甜点值比这个更大并不会带来明显提升反而可能因为内存带宽饱和而维持不变。mmap的优势在于随机访问场景而不是顺序拷贝。所以如果你在优化IO性能不要迷信mmap先试试把缓冲区调到64KB~1MB往往就能解决大部分问题。6. 实战中的坑与排查技巧6.1 目录IO为什么不能在目录里写文件前面提到目录也是文件。open一个目录能拿到fd但你不能write一个目录。写一个目录会导致EISDIR错误。读取目录内容需要调用readdir在fd的基础上封装了getdents系统调用。目录里没有“普通数据”它的数据是目录项dentry结构内核会禁止你直接写。所以如果有人跟你炫耀“我用write往目录里写了一个文件”那是虚假的。文件系统有自己的结构你不能绕过内核直接去操纵目录。正确创建文件的唯一途径是open删除文件是unlink。这两个操作背后都是文件系统在管理inode。6.2 普通文件末尾没有换行到底算不算一行在Linux里文本文件以换行符\n作为行的结束符。有些编辑器比如老的Windows记事本不强制最后一行有换行但在Linux处理文本比如cat、grep、awk时最后一行没有换行符有时候会导致一些命令行为怪异。比如printf hello test.txt cat test.txt你看到的输出是hello后面没有换行终端提示符会紧跟在hello后面。这不算错误但在处理文本时你要意识到文件末尾有没有换行符会影响一些工具的解析结果。在写文件时如果你需要每行一条记录最好确保每一行都带上\n。6.3 read返回0和返回-1的排查read返回0一定是EOF但网络socket的EOF和普通文件的EOF含义不同——socket的EOF表示对端关闭了连接半关闭文件EOF表示数据读完了。read返回-1要按errno细分EAGAIN/EWOULDBLOCK非阻塞模式下当前无数据可读。EINTR被信号中断应该重试。EBADFfd无效。EFAULTbuf指针非法。EIO底层IO错误可能是磁盘故障。我自己调试问题时最常用的排查命令是strace -e traceread,write,open,close ./your_programstrace能打印每次系统调用的参数和返回值几乎一眼就能看出哪一步出了问题。很多线上诡异问题strace一上去就原形毕露。比如文件描述符泄漏strace能清楚显示open之后没有close的调用序列。6.4 文件描述符泄漏定位神器/proc/PID/fd假设你怀疑某个进程fd泄漏最快的方法是ls -l /proc/PID/fd cat /proc/PID/fdinfo/5/proc/PID/fd目录里每个软链的名字就是fd编号链接目标指向文件路径。fdinfo里的pos字段是当前偏移量flags字段显示了打开标志。我可以根据fd数量、目标路径、标志位判断程序在干什么。比如看到几百个fd都指向同一个日志文件那程序多半是循环打开日志忘记关闭了。有个更粗暴的统计ls /proc/PID/fd | wc -l数一下有多少个fd。如果数量在持续增长那泄漏几乎是实锤。6.5 缓冲带来的坑为什么日志不及时输出日志不输出是排查问题时的常见烦恼。最常见的原因就是stdio的行缓冲/全缓冲。终端上printf带换行能立即显示但你重定向到文件后就变成全缓冲了要等缓冲区满或进程退出才输出。所以很多服务端程序里会写setvbuf(stdout, NULL, _IONBF, 0);把stdout设为无缓冲。这样每次printf都立刻进入内核虽然性能有所下降但日志实时性大大提升。对日志系统来说实时性往往比性能重要。再讲究一点可以自己写一个带级别控制的日志库内部维护fd每次写完主动fsync确保极端情况下也不丢日志。我自己的经验是生产环境日志的可靠性优先级高于性能。很多时候为了能在故障时看到最后几行日志牺牲一点写入吞吐是值得的。7. 实践练习与扩展方向7.1 五个能加深理解的练习如果你学完这部分想检验自己是不是真懂了我建议做这几个练习用read/write实现一个自己的cat命令尽量用64KB的缓冲区。用dup2实现一个./mystat output.txt的输出重定向。写一个程序证明stdio的缓冲区在fork之后的复制行为。用fcntl对fd设置非阻塞标志然后用read读一个没有数据的管道观察EAGAIN。自己写一个带O_APPEND的多进程日志写入程序验证不会互相覆盖。每个练习运行后都用strace看一下系统调用序列留意read/write的返回值。7.2 继续往深了走页缓存、零拷贝与异步IO基础IO学完自然延伸的方向是页缓存原理、mmap与page cache的关系、sendfile和splice这类零拷贝技术、以及io_uring和epoll这类异步IO。这些内容都是围绕“内核如何管理文件数据”展开的而基础IO就是理解它们的先决条件。比如sendfile你原来拷贝一个大文件需要read到用户态再write回内核态数据在用户态和内核态之间来回拷贝白白浪费一次内存复制。sendfile直接在内核里完成两个文件描述符的复制速度更快。理解了基础IO的缓冲区层级你就会理解sendfile为什么快、什么时候该用、什么时候不该用。io_uring是近年来Linux IO的大热门它是异步IO接口可以在不阻塞调用线程的情况下完成大量IO请求。但它底层的操作对象依然是文件描述符你对fd的理解越深用io_uring时踩的坑就越少。7.3 从基础IO到文件系统再往上走一个通用的学习路径是学基础IOopen/read/write/lseek/close。学文件系统概念inode、dentry、page cache。学网络IOsocket是fd的一种特殊类型。学高级IOepoll、io_uring、零拷贝。学存储系统设计重放日志、WAL、LSM树。很多人在第1步就没打牢基础后面学网络编程时一遇到EAGAIN、EINTR、非阻塞读写就晕头转向。其实这些概念的基础还是对fd行为的理解。我强烈建议把基础IO这一章的每一个API都亲手写一遍用strace观察系统调用用/proc观察fd状态该踩的坑都踩一遍之后你会发现自己看网络编程、看并发编程都通透很多。我在实际教学过程中发现凡是能静下心把基础IO这一章啃透的人后面学多路复用、学并发模型、学存储引擎都轻松很多。反之基础IO没学扎实后面每学到新概念都要反反复复回来补课。Linux系统编程的魅力就在于最底层的机制一旦掌握了上层再花哨的框架也只是一个封装而已。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。