我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Linux嵌入式学习——标准IO与文件IO

Linux嵌入式学习——标准IO与文件IO 一、标准 IOC 库函数带缓存1. 基本概念输入输出是以内存为参考点。数据进入内存叫输入离开内存叫输出。标准 IO 是 C 库中关于文件操作的函数集合通常自带缓存区跨平台性好。2. 核心函数FILE *fopen(const char *path, const char *mode)功能打开文件并建立流返回 FILE* 指针。参数 mode 拓展解析r只读模式。文件必须存在否则报错。r读写模式。文件必须存在否则报错。w只写模式。文件不存在则创建文件存在则清空截断为0。w读写模式。文件不存在则创建文件存在则清空。a追加只写模式。文件不存在则创建文件存在则在末尾追加。a追加读写模式。文件不存在则创建文件存在则在末尾追加。读指针初始在开头写操作永远在末尾。int fputc(int c, FILE *stream)功能向流中写入一个字符。参数 c要写入的字符以 int 形式传递 ASCII 码值。int fgetc(FILE *stream)功能从流中读取下一个字符。返回值成功返回字符 ASCII 码转为 int读到文件末尾或失败返回 EOF (-1)。int fputs(const char *s, FILE *stream)功能将字符串 s 写入流中不包含末尾的 \0。char *fgets(char *s, int size, FILE *stream)功能从流中读取字符串。参数 size 解析最多读取 size - 1 个字符或者遇到换行符 \n 停止。读取的内容会自动在末尾补 \0。size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)功能按数据块读取。参数 size 与 nmemb 解析总共读取 nmemb 个数据块每个数据块大小为 size 字节。总读取字节数为 size * nmemb。返回值为实际读取的块数不是字节数。size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)功能按数据块写入。参数 size 与 nmemb 解析总共写入 nmemb 个数据块每个数据块大小为 size 字节。返回值为实际写入的块数。int fseek(FILE *stream, long offset, int whence)功能移动文件指针。参数 whence 拓展解析SEEK_SET从文件开头开始偏移。SEEK_CUR从当前文件指针位置开始偏移。SEEK_END从文件末尾开始偏移。long ftell(FILE *stream)功能返回当前文件指针相对于文件开头的字节偏移量。void rewind(FILE *stream)功能将文件指针复位到文件开头等价于 fseek(stream, 0, SEEK_SET)。3. 标准 IO 的缓存机制与类型全缓存普通文件缓冲区满才写入磁盘通常 4096 字节。行缓存终端设备遇到 \n 或者缓冲区满时写入通常 1024 字节。无缓存标准错误 stderr直接输出。int fflush(FILE *stream)功能强制刷新缓冲区将缓冲区的数据立即写回内核。int fileno(FILE *stream)功能获取标准 IO 流底层的文件描述符fd。FILE *fdopen(int fd, const char *mode)功能将一个已有的文件描述符转换为标准 IO 流FILE*。4. 综合示例#include stdio.h #include string.h #include stdlib.h typedef struct { char name[64]; int age; char address[100]; char phone[20]; } PER; // // 1. 字符与字符串读写 (fputc, fgetc, fputs, fgets) // void test_char_string_io() { // [笔记] fopen mode w: 文件不存在则创建存在则清0只写。 FILE* fp fopen(1.txt, w); if (NULL fp) { perror(fopen error); return; } fputc(o, fp); fputc(k, fp); fputs(how areyou\n, fp); // [注意] 一个汉字通常占3字节这里将 utf-8 字符串写入文件 char buf[256] 你好\n; fputs(buf, fp); fclose(fp); // [笔记] fopen mode r: 文件不存在报错存在则只读。 fp fopen(1.txt, r); if (NULL fp) { perror(fopen error); return; } printf(--- 单字符读取 fgetc 演示 ---\n); while (1) { int c fgetc(fp); if (EOF c) break; // End of file printf(%c, c); } // [重点] 测试 fgets 读取注意大小限制并且会读取换行符 rewind(fp); // [笔记] rewind 把文件读写位置复位到开始的地方 printf(\n--- 字符串读取 fgets 演示 ---\n); while (1) { char s[256] {0}; // [笔记] fgets 从流中获取 size 大小字节读到换行或末尾停止 char *ret fgets(s, sizeof(s), fp); if (NULL ret) break; printf(%s, s); } fclose(fp); } // // 2. 数据块读写与定位 (fread, fwrite, fseek, ftell) // void test_block_io() { PER per {张三, 20, 北京市, 13800000000}; FILE* fp fopen(1.bin, w); if (NULL fp) return; // [笔记] fwrite: 从 ptr 取出 nmemb 个大小为 size 的数据块写入 stream fwrite(per, sizeof(per), 1, fp); fclose(fp); fp fopen(1.bin, r); if (NULL fp) return; PER read_per; // [笔记] fread: 从 stream 获取 nmemb 个大小为 size 的数据块到 ptr fread(read_per, sizeof(read_per), 1, fp); printf(name:%s age:%d addr:%s phone:%s\n, read_per.name, read_per.age, read_per.address, read_per.phone); // [笔记] fseek: 将 stream 文件指针从 whence 位置偏移 offset 字节 fseek(fp, 0, SEEK_END); // [笔记] ftell: 获取当前文件流指针的具体位置一般用于算文件大小 long size ftell(fp); printf(1.bin file size is %ld\n, size); fclose(fp); } // // 3. 命令行参数与文本文件处理 (argc, argv) // void test_cmd_args(int argc, char **argv) { // 模拟命令行./a.out count_lite if (argc 2) { printf(usage:./a.out [filename]\n); return; } // [代码整合] 对应 08count_lite.c (统计小写字母个数) 和 09line_num.c (统计行数) FILE* fp fopen(argv[1], r); if (NULL fp) { perror(fopen error); return; } int num[26] {0}; int line_num 0; while (1) { char buf[256] {0}; if (NULL fgets(buf, sizeof(buf), fp)) break; // 统计行数 if (\n buf[strlen(buf) - 1]) line_num; // 统计字母 for (int i 0; buf[i] ! \0; i) { if (buf[i] a buf[i] z) { num[buf[i] - a]; } } } fclose(fp); printf(line_num is %d\n, line_num); for (int i 0; i 26; i) { printf(%c:%d\n, i a, num[i]); } } int main(int argc, char **argv) { test_char_string_io(); test_block_io(); if (argc 2) { test_cmd_args(argc, argv); } return 0; }二、文件 IO系统调用无缓存1. 基本概念系统调用是 Linux 系统直接提供的接口。没有缓存区适合操作设备文件。操作对象是文件描述符非负整数通常为 0-1023。系统默认打开 0(标准输入)、1(标准输出)、2(标准错误)。2. 核心函数int open(const char *pathname, int flags, mode_t mode)功能获得文件描述符。参数 flags 拓展解析必须包含以下三种之一O_RDONLY只读打开。O_WRONLY只写打开。O_RDWR读写打开。参数 flags 扩展标志可通过 | 组合O_CREAT若文件不存在则创建。此时必须提供第三个参数 mode如 0666。O_TRUNC若文件已存在且以写或读写方式打开则将其长度清空为 0。O_APPEND每次写入操作前文件指针自动移到文件末尾。O_EXCL与 O_CREAT 结合使用若文件已存在则 open 失败用于确保创建的是新文件。ssize_t read(int fd, void *buf, size_t count)功能从文件描述符读取数据到内存。参数 count 解析请求读取的最大字节数。实际读取的字节数可能小于 count如遇到文件末尾。返回值成功返回实际读到的字节数文件末尾返回 0失败返回 -1。ssize_t write(int fd, const void *buf, size_t count)功能将内存数据写入文件描述符。参数 count 解析请求写入的字节数。返回值成功返回实际写入的字节数失败返回 -1。off_t lseek(int fd, off_t offset, int whence)功能定位文件读写指针。参数 whence 同 fseek。返回值成功返回当前指针相对于文件开头的偏移量失败返回 -1。int close(int fd)功能关闭文件描述符释放系统资源。3. errno 与常见错误码机制errno 是全局变量。系统调用失败通常返回 -1时内核会自动修改它的值。成功的系统调用不会将其清零。常见错误码ENOENT (2)No such file or directory文件不存在。EACCES (13)Permission denied权限不足。EEXIST (17)File exists文件已存在配合 O_EXCL 使用。EISDIR (21)Is a directory试图对目录进行写操作。ENOTDIR (20)Not a directory试图把文件当目录操作。EMFILE (24)Too many open files当前进程打开的 fd 达到了上限 1024。4. 综合示例#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h // unix standard #include string.h #include stdlib.h // [笔记] 标准IO 文件IO区别: // 1. 文件IO没有缓存区适合操作设备文件 // 2. 操作对象不是 FILE* 而是文件描述符 (很小的非负整数 0-1023) // 0-STDIN 1-STDOUT 2-STDERR void test_basic_file_io() { // [笔记] open flags: O_WRONLY(只写) | O_CREAT(创建) | O_TRUNC(清空) // mode: 0666 权限 int fd open(io_test.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666); if (-1 fd) { perror(open error); return; } char buf[256] hello, are you ok?; // [笔记] write: 通过文件描述符向文件中写数据 write(fd, buf, strlen(buf)); close(fd); fd open(io_test.txt, O_RDONLY); if (-1 fd) return; // [笔记] read: 返回值 成功返回读到数据的个数, 失败-1, 读到文件结尾返回0 while (1) { char read_buf[256] {0}; int ret read(fd, read_buf, sizeof(read_buf)); if (ret 0) break; printf(ret is %d, buf:[%s]\n, ret, read_buf); } // [笔记] lseek: 定位光标位置 (SEEK_SET, SEEK_CUR, SEEK_END) off_t off lseek(fd, 0, SEEK_END); printf(file size by lseek is %ld\n, off); lseek(fd, 0, SEEK_SET); char c 0; read(fd, c, 1); printf(first char: %c\n, c); close(fd); } // [代码整合] 对应 05cp.c (基于文件IO实现复制) void test_cp() { int srcfd open(io_test.txt, O_RDONLY); int dstfd open(io_test_copy.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666); if (-1 srcfd || -1 dstfd) return; while (1) { char buf[4096] {0}; int ret read(srcfd, buf, sizeof(buf)); if (ret 0) break; write(dstfd, buf, ret); // 注意写入的字节数是实际读到的 ret } close(srcfd); close(dstfd); printf(File copied.\n); } // [代码整合] 对应 06insert.c (中间插入数据) void test_insert() { int pos 6; char insert_buf[10] ok; // [笔记] O_RDWR 读写模式 int fd open(io_test.txt, O_RDWR); if (-1 fd) return; off_t size lseek(fd, 0, SEEK_END); // 分配足够空间存放插入点之后的数据 char *data malloc(size - pos); if (NULL data) return; lseek(fd, pos, SEEK_SET); int ret read(fd, data, size - pos); // 读出后半段 lseek(fd, pos, SEEK_SET); write(fd, insert_buf, strlen(insert_buf)); // 写入插入内容 write(fd, data, ret); // 把后半段追加回来 free(data); close(fd); } int main() { test_basic_file_io(); test_cp(); test_insert(); return 0; }三、目录操作与文件属性1. 目录遍历DIR *opendir(const char *name)功能打开目录返回目录流指针。struct dirent *readdir(DIR *dirp)功能读取目录中的下一个目录项。参数返回解析返回的 dirent 结构体中d_name 为文件名d_type 为文件类型DT_DIR 目录DT_REG 普通文件DT_BLK 块设备等。int closedir(DIR *dirp)功能关闭目录流。2. 路径与目录管理int chdir(const char *path)功能改变当前工作目录。char *getcwd(char *buf, size_t size)功能获取当前绝对工作路径存入 buf 中。int mkdir(const char *pathname, mode_t mode)功能创建新目录。mode 参数如 0777。目录权限 rwx 拓展解析r可以读取目录内的文件列表ls。w可以在目录内创建、删除、重命名文件touch、rm。x可以进入该目录cd或者访问目录内的文件。int rmdir(const char *pathname)功能删除目录。注意被操作的目录必须为空目录。int remove(const char *pathname)功能删除文件或空目录比 rmdir 更通用。int rename(const char *oldpath, const char *newpath)功能重命名文件或目录也可用于移动文件。int access(const char *pathname, int mode)功能检查文件是否存在或具备某种权限。参数 mode 解析F_OK(存在性)、R_OK(读权限)、W_OK(写权限)、X_OK(执行权限)。3. 属性获取int stat(const char *path, struct stat *buf)功能获取文件属性。如果文件是软链接会追踪到目标文件。参数返回解析struct stat 核心成员st_size文件大小字节。st_mode文件类型和权限。可用宏 S_ISREG() 判断普通文件S_ISDIR() 判断目录S_ISLNK() 判断软链接等。权限位可用 S_IRUSR、S_IWUSR 等宏进行按位与()判断。st_nlink硬链接数。st_mtim最后修改时间Modify。st_atim最后访问时间Access。st_ctim状态改变时间Change如修改权限。int lstat(const char *path, struct stat *buf)功能与 stat 相同但如果文件是软链接获取的是软链接本身的属性不追踪目标。4. umask 与权限掩码八进制法则rwx 对应 4-2-1。0666 (rw-rw-rw-)0777 (rwxrwxrwx)0644 (rw-r--r--)。umask 机制系统默认掩码通常 0022。创建文件时实际权限 mode ~umask。例如 open 传 0666经 0022 掩码后实际为 0644。四、时间与错误处理1. 时间相关time_t time(time_t *t)功能获取从 1970-01-01 00:00:00 到现在的秒数Unix 时间戳。struct tm *localtime(const time_t *timep)功能将秒数转化为本地日历时间。参数返回解析struct tm 中 tm_year 需加 1900 才是真实年份tm_mon 需加 1 才是真实月份。char *ctime(const time_t *timep)功能将秒数直接转化为可读的日期字符串。2. 错误处理perror(const char *s)功能打印 s 字符串加上 errno 对应的系统错误信息到标准错误输出stderr。char *strerror(int errnum)功能传入错误码返回对应的错误描述字符串。3. 二进制文件解析与内存对齐#pragma pack(1) 或 #pragma pack(push, 1)功能强制结构体按 1 字节对齐。作用在解析 BMP 等二进制文件头时防止编译器默认的字节填充Padding导致读取的数据错位。4. 综合示例三、四#include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include sys/stat.h #include dirent.h #include time.h #include errno.h #include string.h void test_dir_ops() { // [笔记] opendir: 打开目录获得目录流指针(DIR*)表示整个目录信息 DIR *dir opendir(.); if (NULL dir) { perror(opendir error); return; } // [笔记] readdir: 从目录流中读取文件信息(dirent 结构体) while (1) { struct dirent *info readdir(dir); if (NULL info) break; // [笔记] d_type 判断文件类型 switch (info-d_type) { case DT_DIR: printf(目录文件); break; case DT_REG: printf(普通文件); break; case DT_BLK: printf(块设备); break; default: printf(其他类型); } printf(%s\n, info-d_name); } // [笔记] closedir: 关闭目录流 closedir(dir); } void test_path_ops() { char buf[512] {0}; // [笔记] getcwd: 获得当前工作路径 getcwd(buf, sizeof(buf)); printf(before cd :%s\n, buf); // [笔记] chdir: 切换当前工作路径 int ret chdir(../); if (-1 ret) { perror(chdir error); } getcwd(buf, sizeof(buf)); printf(after cd :%s\n, buf); // [笔记] mkdir: 新建目录 (0777 权限) mkdir(test_mkdir_123, 0777); // [笔记] rmdir: 删除目录 (必须为空目录) rmdir(test_mkdir_123); } void test_stat_time_err() { struct stat st; // [笔记] stat: 获取文件属性最常用成员 st_size 表示大小(字节) int ret stat(./01ls.c, st); // 如果没有这个文件会报错 if (-1 ret) { // [笔记] perror: 打印 s 字符串和 errno 对应的错误信息到 stderr perror(stat); // [笔记] strerror: 将错误码 errno 转为字符串 printf(errno:%d errmsg:%s\n, errno, strerror(errno)); } else { printf(size: %lu bytes\n, st.st_size); } // [笔记] time: 记录 Unix 时间戳 (从1970-1-1 0:0:0开始的秒数) time_t tm time(NULL); printf(timestamp: %lu\n, tm); // [笔记] localtime: 将秒数转化为日历时间(struct tm) struct tm* tminfo localtime(tm); printf(%d-%02d-%02d %02d:%02d:%02d\n, tminfo-tm_year 1900, tminfo-tm_mon 1, tminfo-tm_mday, tminfo-tm_hour, tminfo-tm_min, tminfo-tm_sec); // [笔记] ctime: 将时间秒数直接转化为字符串 (类似 date 命令) char *data ctime(tm); printf(%s, data); } int main() { test_dir_ops(); test_path_ops(); test_stat_time_err(); return 0; }五、易错点总结1.fgets 会保留行末的换行符 \n如果缓冲区没满且读到了换行处理字符串时经常需要手动将 \n 替换为 \0。2. read 系统调用不会自动在读取内容的末尾补 \0。如果读取的是文本必须手动将 buf[ret] 置为 \0 才能用 printf 打印。3. stat 解析文件权限时绝对不能使用 比较如 if(st.st_mode S_IRUSR) 错误必须使用按位与如 if(st.st_mode S_IRUSR)。4. 对结构体直接进行 fread/fwrite 时必须注意内存对齐padding问题必要时使用 #pragma pack(1) 取消对齐否则跨平台或不同编译器读取会错位。5. 使用 open 创建文件时O_CREAT必须提供第三个参数 mode如 0666否则系统会给一个随机或错误的权限。6. 文件 IO 和标准 IO 不要混用。同一个文件同时用 FILE* 和 fd 操作会导致缓存区和内核指针不同步。如果需要混用应先用 fflush 刷新缓冲区或通过 fileno/fdopen 进行转换。7. 系统调用成功时不会自动把 errno 清零。排查错误时不要依赖 errno 是否为 0 来判断上一次调用是否成功而应直接判断函数的返回值。8. umask 会影响最终创建文件的权限。如果发现 open 传入的 mode 和最终文件权限不符检查当前终端的 umask 值终端输入 umask 命令查看。
返回列表