【从浅学到熟知Linux】基础IO第一弹=>C语言文件操作接口、文件系统调用、文件描述符概念及分配规则

本文主要是介绍【从浅学到熟知Linux】基础IO第一弹=>C语言文件操作接口、文件系统调用、文件描述符概念及分配规则,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

🏠关于专栏:Linux的浅学到熟知专栏用于记录Linux系统编程、网络编程等内容。
🎯每天努力一点点,技术变化看得见

文章目录

  • C语言文件接口回顾
  • 系统文件概念与接口
    • 文件基本概念
    • 系统接口
      • open
      • read
      • write
      • close
      • lseek
    • 什么是当前路径
  • 文件描述符
    • 文件描述符概念与原理
    • 文件描述符分配规则


C语言文件接口回顾

在开始介绍基础IO上篇的相关内容前,让我们先巩固一下C语言的文件操作

C语言中打开文件的方式及区别如下标所示↓↓↓

打开模式描述
r只读方式打开
r+以读写方式,读写开始位置默认在文件开始
w以写方式打开,文件不存在则创建,存在则清空
w+以读写方式打开,文件不存在则创建,存在则清空
a以追加方式打开,文件不存在则创建,文件存在则在文件末尾追加写入
a+以追加与读方式打开,文件不存在则创建,读写位置默认在文件尾

下面是三个读写文件的函数

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int fprintf(FILE *stream, const char *format, ...);

【代码示例1】r(只读)方式打开,结合fread函数↓↓↓

#include <stdio.h>int main()
{FILE* fp = open("./log.txt", "r");char buffer[1024];fread(buffer, sizeof(buffer), sizeof(char), fp);printf("%s\n", buffer);fclose(fp);return 0;
}

在这里插入图片描述
【代码示例2】r+(读写)方式打开,结合fwrite、fread函数↓↓↓

#include <stdio.h>
#include <string.h>int main()
{FILE* fp = fopen("./log.txt", "r+");char* msg = "xm";fwrite(msg, strlen(msg), sizeof(char), fp);char buffer[1024];fread(buffer, sizeof(buffer) - 1, sizeof(char), fp);printf("%s\n", buffer);fclose(fp);return 0;
}

在这里插入图片描述
【代码示例3】w(写)方式打开,结合fprintf函数↓↓↓

#include <stdio.h>int main()
{FILE* fp = fopen("./log.txt", "w");fprintf(fp, "jammingpro\n");fclose(fp);return 0;
}

在这里插入图片描述
【代码示例3】w+(读写)方式打开,结合fseek、fread、fwrite函数↓↓↓

★ps:fseek的接口声明如下↓↓↓

int fseek(FILE *stream, long offset, int whence);

其中whence和offset可填写值和关系如下:

whenceoffset
SEEK_SET从文件开始位置向后偏移n个位置
SEEK_CUR从当前位置向后偏移n个位置
SEEK_END从文件结尾位置向前偏移n个位置
#include <stdio.h>
#include <string.h>int main()
{FILE* fp = fopen("./log.txt", "w+");char* msg = "jammingpro\n";fwrite(msg, strlen(msg), sizeof(char), fp);fseek(fp, 0, SEEK_SET);char buffer[1024];fread(buffer, sizeof(buffer), sizeof(char), fp);printf("%s", buffer);fclose(fp);return 0;
}

在这里插入图片描述
【代码示例4】a(追加)方式打开,结合fwrite函数↓↓↓

#include <stdio.h>
#include <string.h>int main()
{FILE* fp = fopen("./log.txt", "a");char* msg = "Jammingpro\n";fwrite(msg, strlen(msg), sizeof(char), fp);fclose(fp);return 0;
}

在这里插入图片描述
【代码示例5】a+(读写)方式打开,结合fseek、fread、fwrite函数↓↓↓

#include <stdio.h>
#include <string.h>int main()
{FILE* fp = fopen("./log.txt", "a+");char* msg = "Jammingpro\n";fwrite(msg, strlen(msg), sizeof(char), fp);fseek(fp, 0, SEEK_SET);char buffer[1024];fread(buffer, sizeof(buffer), sizeof(char), fp);fclose(fp);return 0;
}

在这里插入图片描述

系统文件概念与接口

文件基本概念

当我们使用ls -l查看文件信息时,会显示文件的类型、权限、引用计数、所有者、所属组、大小、创建时间等信息,这些都是文件一些属性信息。
在这里插入图片描述
由此,我们可以知道:文件=内容+属性

文件被打开,需要先被加载到内存。由于内存空间有限,不可能将所有文件全部打开,所以文件可分为已经被打开的文件和没有被打开的文件。

对于打开的文件,由于文件必然是被进程打开的,因而我们需要研究文件与进程之间的关系;对于没有打开的文件,这些文件一定存放于磁盘中,这么多的未打开文件如何被分门别类的存放于磁盘中,要如何快速找到它们并对它们做增删改查操作是我们需要研究的问题。对于打开的文件,将在该文章中介绍,未打开的文件将在下一篇文章介绍。

系统接口

文件其实是在磁盘上的,磁盘属于外部设备,访问磁盘上的文件就是访问磁盘。进程要对文件做操作就需要访问硬件设备,而整个计算机中只有操作系统具有这种权力,操作系统会提供对应的调用接口用于访问对应的硬件设备。下面介绍一下操作系统提供的文件操作接口↓↓↓

open

在这里插入图片描述
open系统接口第一个参数需要传入待打开文件的路径,第二个参数表示以什么样的方式打开,第二个参数可填写的选项如下标所示↓↓↓

flags选项描述
O_RDONLY只读
O_WRONLY只写
O_CREAT不存在就创建
O_TRUNC清空文件
O_APPEND追加写入

这些选项如何使用?使用原理是什么呢?它的原理就是位图结构。一个int类型包含32个比特位,如果我们让低位起第1位为1表示READ,低位起第2位为1表示WRITE…(如下图所示)
在这里插入图片描述
此时若要实现CREAT、TRUNC、WRITE,则只要对这三个数做或运算,即CREAT | TRUNC | WRITE,则会得到一个值为00000000 00000000 00000000 00011010的flags。将它传递给处理函数中,处理函数会将flags与READ、WRITE、APPEND等,挨个做与运算,如果与出来的结果不为0,则表示该选项被选择。

#include <stdio.h>#define READ	(1 << 0)
#define WRITE	(1 << 1)
#define APPEND	(1 << 2)
#define CREAT	(1 << 3)
#define TRUNC	(1 << 4)void run(int flags)
{if(flags & READ){printf("reading...\n");}if(flags & WRITE){printf("writing...\n");}if(flags & APPEND){printf("appending...\n");}if(flags & CREAT){printf("creating...\n")}if(flags & TRUNC){printf("truncing...\n");}
}int main()
{run(READ);printf("------------------------------------\n");run(WRITE | TRUNC | CREAT);printf("------------------------------------\n");run(APPEND | CREAT);printf("------------------------------------\n");return 0;
}

在这里插入图片描述

而open的第三个参数表示表示文件创建时的权限。当我们使用touch命令创建文件时,我们习以为常的认为,文件的默认权限是666,文件夹的默认权限是777。但在使用系统接口时,我们需要显示指明文件权限,系统接口没有给我们设置默认权限。

★ps:系统文件权限默认是666,目录默认权限是777。但它们的默认权限需要与~umask做与运算,即文件的最终权限等于666&~mask,目录的最终权限是777&~umask。我们可以使用umask接口手动设置umask,而不使用当前系统的默认umask。(umask的修改只在该程序内有效)
在这里插入图片描述
下面,我们使用系统接口open以O_RDONLY | O_CREAT创建一个文件↓↓↓(由于没有写明创建文件时的权限,故创建的文件权限是随机的)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("./log.txt", O_RDNOLY | O_CREAT);printf("%d\n", fd);return 0;
}

在这里插入图片描述
下面,我们使用系统接口open以O_TRUNC | O_CREAT创建一个权限为0666的文件↓↓↓(在创建文件前,我们需要umask权限掩码清零)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);int fd = open("./log.txt", O_TRUNC | O_CREAT, 0666);printf("%d\n", fd);return 0;
}

在这里插入图片描述

★ps:open打开文件后会返回一个数字,该数字称为文件描述符,用于唯一标识当前进程打开的文件(即同时打开的不同文件的文件标识符不同),下文将对文件描述符做详细介绍

read

在这里插入图片描述
要从某个文件中读取内容时,第一个参数需要传入该文件的文件描述符,第二个参数需要传入接收文件内容的缓冲区首地址,第三个参数表示要从文件中读取多少字节的内容。

下面程序模拟实现了C语言fopen的r(只读)打开模式↓↓↓

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("./log.txt", O_RDONLY);char buffer[1024];read(fd, buffer, sizeof(buffer));printf("%s", buffer);return 0;
}

在这里插入图片描述

write

在这里插入图片描述
要向某个文件中写入内容时,第一个参数需要传入该文件的文件描述符,第二个参数需要传入将要写入文件的字符串的地址,第三个参数表示要向文件中写入多少字节的内容。

下面程序模拟实现C语言fopen的w(只读)模式打开↓↓↓

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("./log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);char* msg = "Have a good day!\n";write(fd, msg, strlen(msg));return 0;
}

在这里插入图片描述
下面程序模拟实现C语言fopen的a(追加)模式打开↓↓↓

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("./log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);char* msg = "Have a good day!\n";write(fd, msg, strlen(msg));return 0;
}

在这里插入图片描述

close

在这里插入图片描述
close用于关闭文件描述符对应的文件,它只需要传入文件描述符即可。该处不做代码演示,在lseek的示例代码中会演示close的使用。

lseek

在这里插入图片描述
lseek与C语言的fseek使用方法类似,用于移动文件的读写位置,不同之处在于fseek第一个参数传入的是FILE*类型的FILE结构体指针,而lseek第一个参数传入的是文件描述符。第三个描述符仍可以填写以下三个中的一个:SEEK_SET(文件头)、SEEK_CUR(当前位置)、SEEK_END(文件尾),而第二个参数表示偏移量。

下面给出代码示例↓↓↓

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("./log.txt", O_RDONLY | O_APPEND, 0666);char buffer[1024];read(fd, buffer, sizeof(buffer));printf("%s", buffer);//回到文件头再读一遍lseek(fd, 0, SEEK_SET);read(fd, buffer, sizeof(buffer));printf("%s", buffer);close(fd);return 0;
}

在这里插入图片描述

什么是当前路径

当我们没有指名open打开文件的路径时,则open将在当前路径下创建文件↓↓↓

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_CREAT);close(fd);sleep(60);return 0;
}

在这里插入图片描述
将程序运行起来后,我们可以使用ps axj | grep test查看运行该程序的进程pid,进入/proc/进程pid目录,可以看到两个链接文件cwd和exe。其中,cwd是程序的工作路径,也就是我们常说的当前路径,而exe是可自行程序的保存位置。
在这里插入图片描述
当程序运行起来后,默认的工作路径是执行该程序时bash所处的路径。如果我们位于/home/xiaoming路径下执行上述程序,则cwd则指向/home/xiaoming。
在这里插入图片描述
★ps:几乎所有的库只要访问硬件设备,必须要封装系统调用。故C语言的文件操作接口是对上述系统调用接口的封装。

文件描述符

文件描述符概念与原理

通过上述内容,我们知道了文件描述符就是一个整数。下面我们来聊一聊文件描述符的内容。

C语言程序运行时,默认会打开3个输入输出流,我们可以使用一段代码来验证一下↓↓↓

★ps:C语言中接收fopen返回值的类型是FILE,C语言库函数是对系统接口的封装,故FILE中必然存在一个保存描述符的变量,即_fileno。

#include <stdio.h>int main()
{printf("stdin->%d\n", stdin->_fileno);printf("stdout->%d\n", stdout->_fileno);printf("stderr->%d\n", stderr->_fileno);return 0;
}

在这里插入图片描述

C语言默认打开3个输入输出流是语言特性吗?其实不是,这是操作系统的特性。操作系统中,进程创建时默认会打开0、1、2号描述符,它们的详细内容如下标↓↓↓

文件描述符描述对应设备
0标准输入键盘
1标准输出显示器
2错误输出显示器

下面,我们使用系统接口从0号描述符读入内容,并将读入内容写入1号及2号描述符↓↓↓

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>int main()
{char buffer[1024];read(0, buffer, sizeof(buffer));printf("write to 1st fd:\n");write(1, buffer, strlen(buffer));printf("write to 2nd fd:\n");write(2, buffer, strlen(buffer));return 0;
}

在这里插入图片描述

★ps:向1号和2号描述符中打印都是输出到显示器,那它们有什么区别呢?
当我们执行上述程序时使用./sysfileno > ouput.txt,再查看文件输出结果:打印到1号描述符的(printf默认打印到1号描述符)内容被写入到output.txt,而打印到2号描述符的内容的内容被输入到屏幕了。
在这里插入图片描述
./sysfileno > ouput.txt./sysfileno 1> ouput.txt是等价的,两者没有区别↓↓↓
在这里插入图片描述
那如果要将打印到2号描述符的内容打印到err.txt中,打印到1号描述符的内容打印到屏幕呢?则需要执行:./sysfileno 2> ouput.txt↓↓↓
在这里插入图片描述
如果需要将写入到1号和2号文件描述符的内容输入到result.txt,则需要执行./sysfileno > result.txt 2>&1./sysfileno 2> result.txt 1>&2↓↓↓(两者输出结果相同)
在这里插入图片描述
在这里插入图片描述

当操作系统中打开大量的文件时,就需要对打开的文件做管理。在Linux系统中,使用struct file结构体保存打开文件的信息;而对于每个进程来说,每个进程都维护着一个struct files_struct,struct files_struct保存着当前进程打开的各个文件的struct file地址。(每个打开的文件,在内核当中都有file对象,保存了文件相关的inode元信息)ps:inode将于专栏下一篇文章介绍

在这里插入图片描述
现在知道:文件描述符就是从0开始的小整数(因为数组的下标是从0下标开始的)。当打开文件时,操作系统在内存中要创建相应的结构体来描述目标文件,于是就有了file结构体(用于表示一个已经打开的文件对象)。当进程打开文件时,为了让进程和该进程打开的文件产生关联,每个进程就有了一个*files,指向一张files_struct表,该表最重要的部分是:包含了一个保存已打开文件的file结构体地址的指针数组。

所以,本质上,文件描述符就是数组下标(即当前进程的文件描述符表下标)。只要取得对应的文件描述符,就能找到对应的文件。

文件描述符分配规则

那我们打开一个新的文件时,是如何分配文件描述符的呢?↓↓↓

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>int main()
{printf("stdin->%d\n", stdin->_fileno);printf("stdout->%d\n", stdout->_fileno);printf("stderr->%d\n", stderr->_fileno);int fd1 = open("./log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd1->%d\n", fd1);//关闭标准输入close(0);int fd2 = open("./log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd2->%d\n", fd2);//关闭标准错误close(2);int fd3 = open("./log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd3->%d\n", fd3);close(fd1);close(fd2);close(fd3);return 0;
}

在这里插入图片描述
系统默认打开三个标准输入输出流stdin(0号)、stdout(1号)、stderr(2号),此时我们打开一个文件时分配3号描述符,因为0到2号均被占用;关闭0号,再打开一个新文件时,该新文件的文件描述符为0;关闭2号,再打开一个新闻界时,该文件的文件描述符为2。由此可知,文件描述的分配规则是:分配当前最小的未使用的文件描述符

🎈欢迎进入从浅学到熟知Linux专栏,查看更多文章。
如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d

这篇关于【从浅学到熟知Linux】基础IO第一弹=>C语言文件操作接口、文件系统调用、文件描述符概念及分配规则的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/913280

相关文章

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

Linux磁盘分区、格式化和挂载方式

《Linux磁盘分区、格式化和挂载方式》本文详细介绍了Linux系统中磁盘分区、格式化和挂载的基本操作步骤和命令,包括MBR和GPT分区表的区别、fdisk和gdisk命令的使用、常见的文件系统格式以... 目录一、磁盘分区表分类二、fdisk命令创建分区1、交互式的命令2、分区主分区3、创建扩展分区,然后

Linux中chmod权限设置方式

《Linux中chmod权限设置方式》本文介绍了Linux系统中文件和目录权限的设置方法,包括chmod、chown和chgrp命令的使用,以及权限模式和符号模式的详细说明,通过这些命令,用户可以灵活... 目录设置基本权限命令:chmod1、权限介绍2、chmod命令常见用法和示例3、文件权限详解4、ch

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

SpringBoot操作spark处理hdfs文件的操作方法

《SpringBoot操作spark处理hdfs文件的操作方法》本文介绍了如何使用SpringBoot操作Spark处理HDFS文件,包括导入依赖、配置Spark信息、编写Controller和Ser... 目录SpringBoot操作spark处理hdfs文件1、导入依赖2、配置spark信息3、cont

Linux内核之内核裁剪详解

《Linux内核之内核裁剪详解》Linux内核裁剪是通过移除不必要的功能和模块,调整配置参数来优化内核,以满足特定需求,裁剪的方法包括使用配置选项、模块化设计和优化配置参数,图形裁剪工具如makeme... 目录简介一、 裁剪的原因二、裁剪的方法三、图形裁剪工具四、操作说明五、make menuconfig

Golang使用minio替代文件系统的实战教程

《Golang使用minio替代文件系统的实战教程》本文讨论项目开发中直接文件系统的限制或不足,接着介绍Minio对象存储的优势,同时给出Golang的实际示例代码,包括初始化客户端、读取minio对... 目录文件系统 vs Minio文件系统不足:对象存储:miniogolang连接Minio配置Min

Linux使用nohup命令在后台运行脚本

《Linux使用nohup命令在后台运行脚本》在Linux或类Unix系统中,后台运行脚本是一项非常实用的技能,尤其适用于需要长时间运行的任务或服务,本文我们来看看如何使用nohup命令在后台... 目录nohup 命令简介基本用法输出重定向& 符号的作用后台进程的特点注意事项实际应用场景长时间运行的任务服

使用JavaScript操作本地存储

《使用JavaScript操作本地存储》这篇文章主要为大家详细介绍了JavaScript中操作本地存储的相关知识,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下... 目录本地存储:localStorage 和 sessionStorage基本使用方法1. localStorage

使用JavaScript将PDF页面中的标注扁平化的操作指南

《使用JavaScript将PDF页面中的标注扁平化的操作指南》扁平化(flatten)操作可以将标注作为矢量图形包含在PDF页面的内容中,使其不可编辑,DynamsoftDocumentViewer... 目录使用Dynamsoft Document Viewer打开一个PDF文件并启用标注添加功能扁平化