Linux 系统编程入门:文件 IO 深度实操与工程应用
Linux 系统编程入门文件 IO 深度实操与工程应用一、引言很多刚从单片机转型到嵌入式 Linux 的兄弟第一道坎往往不是 Linux 内核而是 Linux 系统编程。在裸机开发里你操作外设就是写寄存器但在 Linux 下万物皆文件——打开/dev/i2c-1就能读写 I2C 设备操作/dev/ttyS0就能收发串口数据。所以文件 IO 是嵌入式 Linux 入门的必修课。今天咱们就从最底层的系统调用open/read/write/close讲起把文件描述符、阻塞与非阻塞、缓冲区这些核心概念彻底捋清楚最后给出一个工程中真能用的串口日志记录模块。二、什么是文件描述符文件描述符File DescriptorFD就是内核给每个已打开文件分配的整数编号。内核通过它来追踪你打开了哪些文件、读到哪儿了、是什么模式打开的。文件描述符宏定义含义0STDIN_FILENO标准输入键盘1STDOUT_FILENO标准输出屏幕2STDERR_FILENO标准错误输出用一个比喻你去图书馆借书管理员内核给你一个号牌——这个号牌就是文件描述符。以后你还书、续借都靠这个号牌沟通。你不需要知道书在哪个架子上管理员帮你搞定。关键点每个进程的文件描述符是独立的。进程 A 的 FD3 和进程 B 的 FD3 可能指向完全不同的文件。内核在每个进程的task_struct中维护一个files_struct表来管理这些映射。三、核心系统调用详解3.1 open() —— 打开文件#includefcntl.hintopen(constchar*pathname,intflags);intopen(constchar*pathname,intflags,mode_tmode);flags 常用组合flag含义O_RDONLY只读O_WRONLY只写O_RDWR读写O_CREAT文件不存在则创建O_TRUNC打开时清空文件内容O_APPEND追加模式写入自动到末尾O_NONBLOCK非阻塞模式O_SYNC同步写入数据必须落盘才返回权限 mode 的值// rw-r--r-- 644open(test.txt,O_WRONLY|O_CREAT|O_TRUNC,0644);注意第三个参数mode需要和当前进程的umask做位与运算后才是最终权限。比如umask是0002你传0666最终创建的文件权限是0664。3.2 read() —— 读取数据#includeunistd.hssize_tread(intfd,void*buf,size_tcount);返回值含义 0实际读取的字节数 0读到文件末尾EOF -1出错查看errno当errno EAGAIN且 fd 是非阻塞模式表示当前没有数据可读新手最容易踩的坑以为一次read就能读完所有数据。实际上read的返回值可能小于你请求的count这在网络编程和串口通信中尤其常见。3.3 write() —— 写入数据ssize_twrite(intfd,constvoid*buf,size_tcount);和read类似write的返回值也可能小于请求写入的字节数需要循环写入来确保数据全部发出。3.4 close() —— 关闭文件intclose(intfd);看似简单却有大学问进程退出时所有打开的文件描述符都会被内核自动关闭。但显式close是好习惯避免文件描述符泄漏——尤其在长时间运行的后台服务中FD 泄漏会导致资源耗尽。四、阻塞 vs 非阻塞你必须懂的区别这俩概念贯穿整个 Linux 编程必须彻底搞懂。阻塞Blocking默认调用read时如果没有数据进程会挂起等待直到有数据到达或出错。非阻塞Non-blocking调用read时如果没有数据立即返回 -1同时errno设为EAGAIN或EWOULDBLOCK。设置非阻塞的方式// 方式1open 时指定intfdopen(/dev/ttyS0,O_RDWR|O_NONBLOCK);// 方式2用 fcntl 动态修改intflagsfcntl(fd,F_GETFL,0);fcntl(fd,F_SETFL,flags|O_NONBLOCK);在嵌入式传感器采集场景中非阻塞模式很常用——你不能让整个采集线程卡在一个read上别的传感器还要不要读了五、工程实战串口日志记录模块下面给一个相对工程化的例子——一个简单的日志记录器支持文件打开、追加写入、自动关闭。#includestdio.h#includestdlib.h#includestring.h#includefcntl.h#includeunistd.h#includestdarg.h#includetime.h#includeerrno.h#defineLOG_PATH/var/log/embedded_sensor.log#defineMAX_LINE256staticintlog_fd-1;/* 初始化日志文件 */intlog_init(constchar*path){log_fdopen(path,O_WRONLY|O_CREAT|O_APPEND,0644);if(log_fd0){fprintf(stderr,log_init: open failed, %s\n,strerror(errno));return-1;}return0;}/* 写入日志,带时间戳 */intlog_write(constchar*fmt,...){if(log_fd0)return-1;charline[MAX_LINE];intprefix_len,msg_len,total;time_tnowtime(NULL);structtm*tm_infolocaltime(now);/* 时间戳前缀 */prefix_lenstrftime(line,sizeof(line),[%Y-%m-%d %H:%M:%S] ,tm_info);/* 用户消息 */va_list args;va_start(args,fmt);msg_lenvsnprintf(lineprefix_len,sizeof(line)-prefix_len,fmt,args);va_end(args);totalprefix_lenmsg_len;if(write(log_fd,line,total)!total){fprintf(stderr,log_write: write failed\n);return-1;}/* 每条日志后换行 */write(log_fd,\n,1);return0;}/* 关闭日志 */voidlog_close(void){if(log_fd0){close(log_fd);log_fd-1;}}/* 使用示范 */intmain(void){if(log_init(LOG_PATH)0)return-1;log_write(传感器初始化完成,温度%.2f°C,25.3);log_write(数据采样周期1000ms);log_close();return0;}这段代码看起来不复杂但涵盖了open 创建/追加打开O_CREAT | O_APPEND 组合确保日志不会丢失已有内容write 非原子性处理虽然这里简化了实际工程中建议检查write返回值时间戳格式化strftimelocaltime是日志模块标配可变参数va_list实现类似printf的接口六、常见踩坑清单问题原因解决方案文件描述符泄漏打开后忘记 close使用 valgrind 或 lsof 排查read 返回值不对一次 read 不够循环读取直到读完写文件没有内容缓冲未刷新使用 fsync 或 O_SYNClseek 定位混乱多线程共享 FD使用 pread/pwrite权限不对umask 影响open 时指定完整 mode七、总结文件 IO 是 Linux 系统编程的基石。掌握了open/read/write/close这套系统调用你就拿到了进入内核世界的门票。在嵌入式开发中无论是读写传感器数据、记录运行日志还是操作 /dev 下的设备节点本质都是文件 IO。下一课我们把思路从文件扩展到进程看看fork如何让嵌入式系统并行处理多个任务。