网站功能开发费用多少钱,做招聘网站需要资质吗,wordpress多色主题,深圳网站建设(信科网络)#x1f466;个人主页#xff1a;Weraphael ✍#x1f3fb;作者简介#xff1a;目前正在学习c和算法 ✈️专栏#xff1a;Linux #x1f40b; 希望大家多多支持#xff0c;咱一起进步#xff01;#x1f601; 如果文章有啥瑕疵#xff0c;希望大佬指点一二 如果文章对… 个人主页Weraphael ✍作者简介目前正在学习c和算法 ✈️专栏Linux 希望大家多多支持咱一起进步 如果文章有啥瑕疵希望大佬指点一二 如果文章对你有帮助的话 欢迎 评论 点赞 收藏 加关注 目录 一、C语言文件操作1.1 打开文件 --- fopen1.2 关闭文件 --- fclose1.3 文件写入 --- fwrite 二、文件系统调用2.1 文件系统调用和函数的关系2.2 打开文件 --- open2.3 文件关闭 --- close2.4 文件写入 --- write 三、文件描述符3.1 什么是文件描述符3.2 三个标准流3.3 文件描述符的分配规则3.4 引用计数 四、再谈重定向4.1 输出重定向4.2 追加重定向4.3 输入重定向4.4 系统调用接口 --- dup2 五、补充知识5.1 stdout和stderr的区别5.2 为什么Linux下一切皆文件以源码的角度剖析 六、相关代码 一、C语言文件操作
1.1 打开文件 — fopen
#include stdio.h
FILE* fopen(const char* path, const char* mode);path要打开文件的相对路径或绝对路径。注意如果不带路径默认会在当前进程所在路径下创建/打开文件。 mode打开文件的模式常用的模式包括 r只读模式。打开文件用于读取。注意文件不存在表示打开文件失败。w写入模式。文件内容截断为零长度即清空文件内容再写入内容。如果文件不存在系统会新建对应的文件a追加模式。不会清空原有文件内容而在文件内容末尾追加。如果文件不存在系统会新建对应的文件还有很多模式请查看手册man fopen 返回值 打开文件后将返回一个指向 FILE 类型的文件指针指向打开的文件流。 如果打开失败则返回 NULL。 为什么不带路径默认会在当前进程所在路径下创建文件为什么不在其他路径创建文件
首先大家需要明白一个道理文件一定是由进程通过调用函数如fopen()打开的因此文件与进程有关。所以默认情况下如果你不带路径新建文件所处的路径将取决于进程当前的工作目录。
当然了如果你的程序在运行时改变了当前工作目录那么新建文件所处的路径也会相应地改变。 1.2 关闭文件 — fclose
在C语言中当你完成了对文件的读写操作后最后应该使用fclose函数关闭文件。这是一个很重要的步骤因为它确保了操作系统释放文件资源并且在某些情况下确保了写入的数据被刷新到磁盘上。不关闭文件可能会导致资源泄露或数据丢失。
int fclose(FILE* stream);如果成功关闭文件fclose函数返回0。 如果关闭失败返回EOF-1表示出现了错误。
1.3 文件写入 — fwrite
C语言对于文件写入有这几种方式fputc、fputs、fwrite、fprintf 和 snprintf大家可以去man手册查看用法
int fputc (int character, FILE* stream);
int fputs (const char* str, FILE* stream);
int snprintf ( char* s, size_t n, const char* format, ...);这里以fwrite函数为例
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);这个函数将 nmemb 个元素每个元素大小为 size 字节从指针 ptr 所指向的内存块写入到给定的文件流 stream 中。 ptr指向待写入数据的指针。 size数据写入的字节数。 nmemb以size为单位待写入数据的数量。 stream指向FILE对象的指针。 返回值如果返回值与 nmemb 不相等说明写入时发生错误。 这里有一个细节问题strlen需要 1将’\0‘写入到log.txt文件中吗
我们执行以上代码看看log.txt里的内容不 1 然后我们再 1看看效果 后面多了一个类似于乱码的字符^这其实就是\0的二进制。因为vim是一个文本编辑器对于二进制来说当然显示的是乱码啦。 这也说明了我们在向文件写入字符串的时候不需要处理\0只需将字符串的内容写入即可因为字符串以\0结尾是编程语言的规定和文件没半毛钱关系 ~
接下来我们再连续执行程序再看看log.txt的内容 我们不是向文件中写入了多个字符串hello file吗为什么只写入了一个
这是因为我们设置打开文件的模式是w即在向文件写入之前会先对文件进行清空处理然后再写入。
这不由得让我们联想到输出重定向 因此输出重定向的底层一定要以w的方式打开一个文件即fopen(文件/文件路径, w)即将文件内容清空最后再用写入函数进行向文件写入。 接下来我们再以a的方式打开一个文件并写入 我们发现以a的方式打开一个文件并写入不会清空文件而是在文件内容末尾追加。
这不由得让我们联想到追加重定向 因此追加重定向的底层一定要以a的方式打开一个文件即fopen(文件/文件路径, a)即不将文件内容清空最后再用任意一个写入函数向文件内容进行追加。
综上所述输出重定向和追加重定向主要区别就是在于打开文件的方式不同一个以w一个以a。 二、文件系统调用
2.1 文件系统调用和函数的关系
我们知道文件是在磁盘上存储的磁盘作为外设外部设备所以访问文件其实也是在访问磁盘硬件而我们计算机体系结构是分层的如下所示 而我们知道底层硬件一定要被操作系统管理的作为普通用户无法直接访问底层硬件的相关信息那么就只能通过管理者也就是操作系统来获取而操作系统根本不相信用户因此提供了系统调用接口来访问以此来保护操作系统。因此几乎所有的库函数只要访问硬件设备必定要封装系统调用接口。因此以上文件操作的函数底层必定封装了文件系统调用接口
2.2 打开文件 — open
在Linux中open函数是一个系统调用用于打开或创建文件。
#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);pathname要打开或创建的文件的路径。不指定路径默认是当前进程的工作路径。 flags控制文件打开方式。以下是常见的打开方式。还有很多打开方式可以通过man 2 open来查看 O_WRONLY以只写模式打开文件。O_CREAT如果文件不存在则创建文件。O_APPEND在写入时将数据追加到文件的末尾。O_TRUNC清空文件内容。 实际上以上的选项是在fcntl.h头文件中定义的宏。我们可以在路径/usr/include/bits查看此路径通常包含了与体系结构相关的宏定义和声明。 mode用于指定新创建文件的权限只有在 O_CREAT 标志位被设置时才有效。一般采用八进制表示的文件权限值例如 0666。 返回值 如果成功返回一个文件描述符非负整数该描述符可以在后续的读写操作中传参。 如果失败返回-1。 当我们将程序运行起来发现open打开文件失败原因是O_WRONLY只会读文件而不会新建文件。那就非常简单了再加一个宏O_CREAT让文件不存在时自动创建即可。可是open的第二个参数flag是整型如何传两个参数
这就要涉及到比特位方式的标志位传递方式。
还记得诸如O_WRONLY这些宏吗它们的共同特点二进制序列中最多只有一个比特位上是1并且1所处的比特位是不同的。而一个整型是4个字节也就是32个比特位标志位那么我们可以使用按位或|运算符来组合多个标志位。比如0001 | 0010 0011。然后open函数内部实现可以使用按位与操作符来检查是否设置了某个标志位比如 因此我们可以对一开始的代码进行修改使用按位或|增加O_CREAT选项 如上我们确实把一个不存在的文件log.txt创建好了可是此文件的权限都是乱码原因在于你新建一个文件此文件要受Linux权限约束你新建文件的权限必须得告诉操作系统。因此只有在 O_CREAT 标志位被设置时需要指定新文件的权限。
在往期博客讲过文件默认的权限是0666即rw-rw-rw- 文件创建出来了并且权限也不是乱码了但是权限并不是我们一开说的rw-rw-rw-。实际上创建出来的文件会收到文件默认权限掩码umask的影响实际创建出来文件的权限值为mode (~umask)而在Linux中umask的默认权限值为0002当我们设置mode值为0666时实际创建出来的文件权限为0666 ~(0002) 0664即rw-rw-r--。
若想创建出文件的权限值不受umask的影响即如上想让权限为rw-rw-rw-那么系统专门提供了 umask系统调用接口可以让你修改默认的权限掩码。
即在创建文件前使用umask函数将文件掩码umask设置为0。 2.3 文件关闭 — close
#include unistd.h
int close(int fd);fd 是要关闭的文件描述符即open函数的返回值。 函数返回值为0表示成功关闭文件返回-1表示出现错误
关闭文件描述符后相关的系统资源将被释放包括文件表项和文件描述符本身。这通常是在不再需要使用文件描述符时执行的操作以释放系统资源并防止资源泄漏。
2.4 文件写入 — write
#include unistd.h
ssize_t write(int fd, const void *buf, size_t count);fd 是文件描述符open函数返回值表示要写入的文件。buf 是一个指向要写入数据的缓冲区的指针。count 是要写入的字节数。返回值是写入的字节数如果出现错误则返回-1
如果想要实现输出重定向的功能不仅需要读O_WRONLY、创建文件O_CREAT在写之前还需要将文件清空O_CREAT 如果想实现追加重定向也非常简单由于清空和追加两个是冲突的因此你只需要将清空O_CREAT替换成追加O_APPEND 【总结】 因此只要是在 Linux 平台中编写的程序无论是什么编程语言在进行文件相关操作时其文件操作函数都有封装系统调用接口。也就是说要想与硬件磁盘打交道必须经过 系统调用 - 操作系统 - 硬件 这条路线否则无法直接与硬件进行交互。
三、文件描述符
3.1 什么是文件描述符
到目前为止我们所知道的文件描述符就是open函数的返回值非负整数这个描述符可以在后续的读写操作中用作文件标识符即找到对应的文件。
那不知道大家有没有思考过一个问题为什么操作系统可以单单通过一个整数文件描述符就能找到对应的文件
文件分为打开的文件和没打开的文件这里先讨论打开的文件等到文件系统再谈没打开的文件。打开的文件是由进程使用函数如fopen()打开的。根据冯诺依曼体系结构被打开的文件一定会被加载到内存。而一个进程可以打开多个文件1:n那么被打开的文件也一定要受操作系统的管理那就要请出管理的六字真言先描述后组织。 描述一个被打开的文件一定要有自己的文件结构体对象file包含文件的信息即struct file { 文件信息; struct file* next; struct file* prev;}; 组织通过双链表方式。 往后对被打开文件的管理就转换成为了对链表的增删改查
所以当进程打开文件时操作系统会在内核中创建数据结构来描述这个已打开的文件对象和PCB类似。这个数据结构通常被称为 file 或其他类似的名字它包含了文件的各种信息如文件位置、权限、状态等。
而进程可以打开多个文件那进程PCB结构体对象就要存储哪些文件是由哪一个进程打开的。因此每个进程PCB对象都要和打开的文件建立关系所以进程PCB对象其实有一个指针struct files_struct* files这个指针指向结构体files_struct而这个结构体包含一个指针数组struct file* fd_array[]这个数组我们可以称之为文件描述符表。数组中的每个元素都是指向当前进程所打开文件的指针地址
所以本质上文件描述符就是指针数组的下标索引。所以只要拿着文件描述符就可以找到对应的文件。 【源码】 我们可以打印出文件描述符来看看 3.2 三个标准流
如上文件描述符即指针数组的下标是从3开始连续递增的。那这里我有一个问题为何不从0开始递增呢难道下标0、1、2存储了其他被打开文件的地址吗你的猜测是正确的
在C语言中默认情况下当一个进程启动时操作系统会打开三个标准流本质上就是文件不同的编程语言都会打开它的三个标准流 stdin标准输入文件描述符 0。通常用来接收用户的输入主要是键盘设备。 stdout标准输出文件描述符 1。通常用来输出程序的正常运行信息主要是显示器设备。 stderr 标准错误文件描述符 3。通常用来输出程序的错误信息stderr 默认也将错误信息显示在终端上。主要也是显示器。 我们可以来验证 FILE是C语言封装文件操作的结构体当你使用 C 标准库中的函数来操作文件时这些库函数底层必定会封装系统调用接口那么FILE结构体内部一定要有对应的文件描述符来与操作系统进行通信即需要有文件描述符来调用底层封装的系统调用接口因此FILE结构体里一定会包含文件描述符成员_fileno其他编程语言的文件操作也是如此 3.3 文件描述符的分配规则
如果是直接打开文件某个文件由于系统默认会打开三个标准流因此新分配的文件描述符从3开始依次递增。 如果我在打开log.txt文件之前把标准输入stdin关闭其文件描述符还会是3吗 我们发现新文件的文件描述符占据了标准输入stdin的文件描述符。因此文件描述符的分配规则是从头遍历文件描述符表fd_array[]找到一个最小的且没有被使用的下标它的下标就是新文件的文件描述符保证数组空间不会被浪费。
3.4 引用计数
如果我这里故意将文件标识符1给关掉即显示器文件被关闭那么打印结果就无法显示到屏幕上 那如果我在文件标识符1关掉的基础上再向stderr流写入即向显示器写入还能写入成功吗有的人想肯定不成功文件标识符1关掉等同将显示器文件关掉而stderr也是将结果显示在显示器肯定打印不出来 我们发现结果可以打印出来。
其实每个文件对象都包含一个引用计数字段count记录了有多少个文件描述符指向该文件对象。当一个进程打开一个文件时内核会增加该文件对象的引用计数count。当进程关闭文件描述符时内核会减少相应文件对象的引用计数count--并且将文件描述符表对应下标的位置置空NULL。只有当引用计数减少到零时内核才会释放该文件对象及其相关资源。 四、再谈重定向
重定向实际上是改变了进程的文件描述符表中指针数组对应下标内容的指向。这样进程在进行I/O操作时就会按照新的文件描述符表中的指向来进行从而实现了重定向的效果。
常见的重定向操作包括 将一个文件描述符指向另一个文件描述符 或者将一个文件描述符关闭。
4.1 输出重定向 输出重定向是指将程序本来输出到标准输出stdout设备的内容重定向到其他文件 此时我们发现本来应该输出到显示器上的内容输出到了文件log.txt当中这不就是典型的输出重定向
接下来我们来分析为什么会出现这种情况
代码中首先将stdout文件关闭而后打开log.txt文件。根据文件描述符的分配规则是从头遍历文件描述符表fd_array[]找到一个最小的且没有被使用的下标它的下标就是新文件的文件描述符保证数组空间不会被浪费。那么log.txt的的文件描述符就是1而后面写入文件是向文件描述符1中写入即就是向log.txt文件写入。这就是重定向的原理。 4.2 追加重定向 追加重定向是指将程序本来输出到标准输出stdout设备的内容追加到指定文件的末尾而不是覆盖原文件内容。 那我们只需将以上代码的打开方式O_TRUNC修改成O_APPEND就有追加重定向的效果 4.3 输入重定向 输入重定向是指将程序从标准输入stdin中读取数据的方式改变为从其他文件读取数据 例如让本应该从“键盘文件”读取数据改从log.txt文件中读取数据那么我们可以打开log.txt文件之前将stdin文件关闭这样一来根据文件描述符的分配规则当我们后续打开log.txt文件时所分配到的文件描述符就是0。 4.4 系统调用接口 — dup2
以上操作都需要先关闭再打开一个文件来实现重定向操作过于繁琐而且每次你写这样的代码都要向别人解释因此操作系统提供了系统调用接口来实现重定向操作
#include unistd.h
int dup2(int oldfd, int newfd); oldfd 是要复制的文件描述符。 newfd 是要被覆盖的文件描述符。 如果 newfd 已经打开则操作系统首先会关闭 newfd即释放文件对象。然后dup2() 会使 newfd 指向 oldfd 所指向的文件对象。
下面是演示了如何使用 dup2() 函数将标准输出重定向到一个文件中 下面是演示了如何使用 dup2() 函数将标准输出追加重定向到一个文件中 下面是演示了如何使用 dup2() 函数将标准输入重定向到一个文件中 为往期模拟实现bash添加重定向功能点击跳转
五、补充知识
5.1 stdout和stderr的区别
我们至今所认知的标准输出流和标准错误流并没有区别都是将数据向显示器打印。 但若是将程序运行结果重定向输出到文件hello.txt当中我们会发现hello.txt文件当中只将标准输出的打印语句重定向到hello.txt文件中而向标准错误流输出的两行数据并没有重定向到文件当中而是仍然输出到了显示器上。 实际上我们使用重定向时默认重定向的是文件描述符是1的标准输出流而并不会对文件描述符是2的标准错误流进行重定向。
此外我们也可以将标准错误流进程重定向通常使用 2 或 2 来重定向 stderr。详细命令如下
./proc 1 hello.txt 2stderr.txt
//不要在重定向操作符和文件描述符之间添加空格。
// 1可以被省略因为它是默认的输出流。上述指令做了两次重定向第一次把标准输出重定向到了文件描述符为1的显示器第二次是把标准错误重定向到了文件描述符为2的显示器通过重定向可以分别将它们发送到不同的目的地使得对程序输出和错误信息的处理更加灵活和有效。
前面已经提到重定向只会默认把标准输出的进行处理并不会重定向标准错误如果我想让标准输出和标准错误一同混合起来到一个文件显示如下命令
./myfile 1all.txt 21这个命令将 myfile的标准输出重定向到 all.txt 文件21 是将标准错误重定向到与标准输出相同的地方也就是all.txt 文件中。
5.2 为什么Linux下一切皆文件以源码的角度剖析
一切皆文件其实是在Linux系统中一致访问资源的方式即将所有资源抽象为文件。这包括了硬件设备等各种资源。文件很容易理解这里主要谈为什么硬件也要当做文件来看待
当一个进程在Linux系统中运行时操作系统会为每个文件创建一个文件对象struct file这个文件对象跟踪了该文件的状态和相关信息。
因此当一个进程打开硬件那么操作系统就会管理这个硬件即为该硬件创建文件对象struct file而此文件对象其实提供了对设备的操作接口。 f_op指向对文件操作的各种函数指针。这些函数指针对应了文件的不同操作例如读取、写入、定位等。 所以所谓的一切皆文件就是相当于在文件这一层封装了一个文件对象让文件对象中的函数指针结构体 file_operations 指向不同设备的操作方法。在这种情况下不同类型的文件对象比如普通文件、设备文件等都具有相同的接口即结构体file_operations。当应用程序调用文件操作函数时操作系统根据文件对象的实际类型来调用相应的方法这种行为类似于多态的表现即同一个接口可以根据不同的对象类型展现出不同的行为。
因此一切皆文件可以被视为在操作系统级别实现的一种多态思想使得不同类型的资源都能够以统一的方式进行访问和管理。
六、相关代码
本篇博客相关代码点击跳转