自学嵌入式第23天(Linux系统编程篇——文件编程)

📅 2026/8/14 16:14:16
自学嵌入式第23天(Linux系统编程篇——文件编程)
Linux系统编程由网络编程文件编程多任务编程组成。在这一篇我们主要学习的是函数的使用函数又有内核函数和库函数两种库函数是别人把内核函数封装好后使用的函数一、Linux系统基础概念1.Linux系统分层架构我们平时写的.c.txt编译出的可执行文件文件都是磁盘上按文件系统规则存储的二进制数据不能直接碰硬件我们写的函数还会调用库函数也就是别人把内核提供的函数进行封装得到的方便我们使用。系统调用是内核专门给用户空间开的官方接口以函数的方式提供从用户到内核的唯一通道。这里就是用户只能通过系统调用访问内核函数和读写硬件寄存器驱动是一个软件驱使硬件动起来的软件程序。内核空间是需要用到操作系统和硬件才会进入。我说实话这里主播也十分懵逼所以如果实在看不懂就先放下我感觉后面学着学着就明白了等这一篇学完我会在总结篇再详细说的希望那个时候我能弄明白这些。二、Linux文件编程在Linux系统中有个叫一切皆文件的说法意思就是把能当做文件的全部当做文件这样的好处是简化了对文件的处理和识别也可以使用统一的操作逻辑。这里操作逻辑是指对所有的文件我们都有先打开文件对文件进行读和写然后再关闭文件如果都看做文件那么我们就只需要进行这几步操作。1.Linux七种文件类型bblock块设备文件直接和硬件关联起来如硬盘或者外接设备如摄像头以块为单位操作一个块为512个字节。cchar字符设备文件以字节为单位其实就是内容输入端相关设备比如键盘ddirectory目录文件-regular常规文件.c.txt图片等等lsymblink连接文件相当于Windows下创建的快捷方式将桌面的一个快捷方式和内存中的软件关联起来。sscoket套接字文件ppipe管道文件让不同的文件可以通信三、文件操作函数接下来进入这一篇的正题下面的内容无论如何都要记住。其实这是两天写了一篇经过几天主播的学习我总结出来其实就是用库函数方式和系统调用方式完成文件打开然后对其进行读写然后再关闭它但是这两种方式都是调用函数只是库函数是经过别人封装的用起来更方便一点系统调用的函数和内核里面的一些东西更近一点。库函数这张图就是用户态标准 IO 库FILE*→ 经过系统调用切换内核态 → 内核三层文件管理结构 → 最终访问磁盘文件的流程stdin标准输入stdout标准输出stderr标准错误这三个文件是默认打开的标准流后面我们通过库函数或者系统调用打开的文件都会有一个文件描述符表fd这个描述符会将当前最小的描述符分给现在打开的这个文件。文件表项中会记录打开模式、文件读写偏移量、引用计数、指向 vnode 的指针vnodeinode节点 存储文件元数据文件大小、权限、磁盘数据块位置等最终指向磁盘上真实文件。这块很重要后面会用到1.fopen头文件#include stdio.h #include fcntl.h形式FILE *fopen(const char* pathname , const char *mode);功能流打开函数打开pathname指定的文件并关联一个流给这个文件。参数pathname是要打开的文件名mode是打开的模式据以有以下几种模式全称读写权限文件不存在文件已存在文件指针初始位置核心特点rread只读打开失败返回 NULL正常打开文件开头不能写入不会清空原有内容wwrite只写创建新空白文件直接清空原有全部内容文件开头慎用打开瞬间就清空旧文件aappend只写追加创建新空白文件保留原有内容文件末尾永远只能在末尾写无法修改前面数据rread可读可写打开失败返回 NULL正常打开文件开头可读可写不会清空文件wwrite可读可写创建新空白文件直接清空原有全部内容文件开头先清空文件之后既能读又能写aappend可读可写创建新空白文件保留原有内容文件末尾写数据永远追加到末尾读可以任意位置返回值成功时返回FILE*指针也就是指向打开文件的指针失败时返回NULL并设置errno可以通过perror“”来打印错误原因到时候会在终端显示“”内信息错误原因2.fclose头文件#includestdio.h形式int fclose (FILE *stream)功能关闭流并释放相关内存空间参数stream是要关闭的文件返回值成功时返回0失败时返回EOFend of file是一个宏定义其实就是-1errno被设置。注意同一个文件不能多次fclose下面为三组读写函数1.fgetc形式int fgetc(FILE *stream)功能单个字符输入函数参数stream是要读取的文件这里实参都是文件指针返回值成功返回读到的字符返回值是int型其实就是字符的ascii码失败返回EOF读到文件末尾也返回EOF。2.fputc形式int fputc (int c,FILE *stream)功能输出单个字符参数c为要输出的字符stream是输出到哪个文件返回值成功时返回输出字符的ascii码失败时返回EOF例子通过这两个函数实现复制文件将第一个文件复制为第二个所以第二个文件只读第二个文件只写fgetc读取一个字符判断是不是EOF不是的话就写入第二个文件。直到读到EOF结束最后关闭两个文件。3.fgets形式char *fgets(char *s,int SIZE,FILE *scream)功能从stream中最多读取SIZE-1个字符放到s中参数s是个字符型数组的指针SIZE是一次读取的字符数返回值成功返回s失败返回NULL读取结束的情况1读到EOF也就是文件末尾2读到\n结束\n会被保存在s中3读到SIZE-1个字符读取结束后会在最后加\0所以只能读SIZE-1个字符4.fputs形式int fputs (const char *s,FILE *stream)功能将s中的内如输出到stream文件输出是不输出\0参数s为要输出的字符串返回值成功时返回非负数失败时返回EOF例子这个是利用fgets和fputs实现的文件复制打开文件和关闭文件部分一样只是读写部分不一样读取buf大小的字节放到buf中如果没读到结尾就将buf中的内容放到目标文件。这里给1024是为了减少内存碎片其实不要这么大也行前面说过fgets遇到\n也会结束这时候buf没读满返回值是buf的地址。下面的这个读写是按二进制读写按对象学习。5.fread形式size_t fread ( void *ptr, size_t size, size_t nmemb, FILE *stream) ;功能:读取二进制数据参数ptr保存数据一块空间size单个元素的大小nmemb元素的个数这里要说的就是字节数元素的大小*元素个数返回值成功返回读取到或写入的元素个数失败返回06.fwrite形式size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) ;功能:写入二进制数据参数ptr保存数据一块空间size单个元素的大小nmemb元素的个数返回值成功返回读取到或写入的元素个数失败返回0例子这里的这个例子不止是复制一个文件了这里还计算了源文件fp_s的字节长度然后生成了一个空洞文件就是一个内容全是\0的文件用来抢占磁盘就是看看磁盘中的位置够不够放下一个这么大的文件如果不够就直接提示内存不够就不复制文件了需要好好理解一下。下面这两个函数都属于定位函数更改的是读写指针的位置。7.fseek形式int fseek (FILE *stream, long offset, int whence);功能:移动文件读写指针的位置用来自由定位文件偏移量常配合 ftell 获取文件大小参数poffset偏移字节数正数向后偏移、负数向前偏移whence偏移基准起点共 3 种取值。SEEK_SET文件开头SEEK_CUR指针当前位置SEEK_END文件末尾返回值定位成功返回 0定位失败返回非 0 数值。注意当偏移基准点为SEEK_SET时偏移量不能为负数当文件为空时桑偏移量基准点时一样的都在开头当偏移量超过文件字节数时还是会偏移这么多但是如果不往文件里面写内容就还是原字节数如果偏移完后写入一个字节那么就会使偏移量1的字节数。8.ftell形式long ftell (FILE *stream);功能:获取当前文件指针距离文件开头的字节偏移量最常用场景是计算文件总长度参数stream需要查询的文件指针返回值正常返回当前偏移字节数出错返回 - 1。void rewind(FILE *stream);也是定位函数将读写指针移动到文件开头。在这个代码中我们进行了将读写指针移动到文件末尾读完长度后与移动到文件开头以及创建一个空洞文件之后又将读写指针移动到文件开头这是为什么呢因为这里有个非常重要的知识点——缓存区。8.缓存标准I0:把文件的操作 抽象了对流的操作所以标准Io都是围绕FILE*指针进行的。并设计缓存---缓冲区为啥缓冲区能提高效率?缓冲区之所以能够提升读写效率本质是因为直接读写磁盘需要调用read、write这类系统调用程序要在用户态与内核态之间来回切换再加上磁盘寻道本身耗时较长倘若每次仅读写一字节就发起一次系统调用频繁的状态切换和磁盘访问会造成极大性能损耗标准IO会在用户空间开辟高速内存缓冲区写入数据时内容会先暂存于缓冲区中暂缓落盘直至缓冲区存满、遇见换行符、程序结束或是手动执行fflush刷新时才将积攒完毕的整块数据通过一次系统调用统一写入磁盘读取数据也会预先批量加载一大段内容到缓冲区后续直接从内存调取这样就把海量细碎的小块IO请求整合为为数不多的大块IO操作大幅削减了用户态内核态切换次数与磁盘访问频次同时内存读写速度远高于硬盘连续批量读写也规避了机械硬盘零散写入的低效问题最终显著提升整体读写效率。那么缓存什么时候会刷新呢主要分为以下三种缓存他们在不同的情况下进行刷新1行缓存大小1k主要用于人机交互stdout行缓存多是关于终端的一些操作刷新条件a.遇到\n刷新b.缓存区满刷新c.程序结束刷新d.fflush刷新fflush(stdout);强制刷新2全缓存4k主要用于文件的读写对普通文件进行标准IO操作建立的缓存一般为全缓存刷新条件a.缓存区满刷新b.程序结束刷新c.fflush来刷新fflush(fp);3不缓存主要用于出错处理信息的输出stderr不对数据缓存直接刷新fprintf(strerr, fopen error %s,filename) ;strerr是界面交互fopen error出错处理由此可知我们在上面的代码中计算文件所占字节数计算完后读写指针处于文件末尾那等我们读这个文件内容时就会读不到内容创建完空洞文件后读写指针也是在文件最后如果不将其放到文件开头那么在写的时候就会接着在\0后面写所占内存也就直接翻倍了。所以在库函数操作文件时一定要时刻注意读写指针的位置。系统调用文件描述符表示打开文件的整数他的分配原则是将最小未使用整数分给当前打开的文件。这里就又要提到这张图了库函数是上层用户空间先缓存攒够数据后才跨过中线触发系统调用系统调用是调用 fd 系列接口直接从系统调用分界线开始向下执行。所以库函数其实就是有个缓冲区在数据达到一定程度在调用系统调用而文件IO系统调用就是直接使用系统调用没有缓存。1.open需要用到的头文件#include sys/types.h#include sys/stat.h#include fcntl.h形式int open(const char *pathname, int flags, mode_t mode);功能:打开文件参数pathname是文件名。flags是打开模式标志位O_RDONLY只读打开O_WRONLY只写打开O_RDWR读写打为必选一个。还可根据需求自己增加附加标志以达到需求多个标志位之间以|(或)连接。O_CREAT文件不存在则新建使用此参数必须带上第三个 mode 参数O_TRUNC文件存在清空原有内容O_APPEND追加写入永远从文件末尾写。mode是新建文件的权限。一遍写为0666具体原因下一篇介绍。返回值成功返回非负整数失败返回-1同时设置 errno 错误码2.close形式int close(int fd);功能:关闭文件参数fd是文件名。返回值成功返回 0失败返回 -13.read形式size_t fread ( void *ptr, size_t size, size_t nmemb, FILE *stream) ;功能:读取数据参数fdopen 返回的有效文件描述符buf缓冲区内存地址读取的数据会存入这块内存count期望读取的字节最大长度返回值0成功读到的字节数 0读到文件末尾 EOF无数据可读 -1读取出错4.write形式 ssize_t write(int fd, const void *buf, size_t count);功能:读取二进制数据参数fd可写的文件描述符buf要写入数据的缓冲区首地址count想要写入的字节数量返回值0实际成功写入的字节数正常等于 count0未写入任何数据-1写入失败5.lseek形式off_t lseek(int fd, off_t offset, int whence);功能:定位读写指针参数int fd 打开文件得到的文件描述符open的返回值off_t offset 偏移字节数有正负正数向文件末尾方向 向后偏移负数向文件开头方向 向前偏移int whence 偏移基准起点只有 3 个固定宏SEEK_SET 文件开头起始位置SEEK_CUR 文件指针当前所在位置 SEEK_END 文件末尾。返回值成功返回调整后新的文件偏移总量距离文件开头的字节数失败返回-1例题这里还是很简单的看看怎么用就行。对比对比项目标准 IO系统调用打开 /关闭fopen、fcloseopen、close读写接口fgetc/fgets/fread、fputc/fputs/fwrite、read、write文件定位fseek、ftell、rewindlseek缓冲区自带用户空间缓冲区行缓冲 / 全缓冲 / 无缓冲无用户缓冲区每次调用直接进入内核操作标识FILE* 文件流指针int 文件描述符 fd优点1. 自带缓冲小块频繁 IO 效率更高2. 接口丰富易用支持格式化输出 printf3. 跨平台Windows/Linux 均可使用1. 无缓存延迟数据可即时落盘2. 占用资源更少底层可控性强4. 无缓冲区数据丢失隐患缺点1. 存在缓冲区延迟未刷新时数据暂存内存程序崩溃易丢数据2. fseek 受缓冲区影响指针偏移逻辑更复杂3. Linux 特殊文件管道、socket适配差1. 频繁小块读写会产生大量用户态内核态切换性能低下2. 没有格式化打印拼接字符串繁琐3. 仅类 Unix 系统可用不跨平台选择在高频IO操作但数据量不是很大的时候使用标准IO低频但数据量大以及操作设备文件时都使用系统调用。怎么说呢主播也是第一次学这个虽然好像全是函数没有很难但是我感觉有很多没听过的名词虽然好像不是很难吧但是还是感觉学的乱七八糟的目前正在努力中如果后面学明白了一定会出一个清晰版的希望能帮助到大家。