【从浅学到熟知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

相关文章

Java操作Word文档的全面指南

《Java操作Word文档的全面指南》在Java开发中,操作Word文档是常见的业务需求,广泛应用于合同生成、报表输出、通知发布、法律文书生成、病历模板填写等场景,本文将全面介绍Java操作Word文... 目录简介段落页头与页脚页码表格图片批注文本框目录图表简介Word编程最重要的类是org.apach

在Linux中改变echo输出颜色的实现方法

《在Linux中改变echo输出颜色的实现方法》在Linux系统的命令行环境下,为了使输出信息更加清晰、突出,便于用户快速识别和区分不同类型的信息,常常需要改变echo命令的输出颜色,所以本文给大家介... 目python录在linux中改变echo输出颜色的方法技术背景实现步骤使用ANSI转义码使用tpu

linux hostname设置全过程

《linuxhostname设置全过程》:本文主要介绍linuxhostname设置全过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录查询hostname设置步骤其它相关点hostid/etc/hostsEDChina编程A工具license破解注意事项总结以RHE

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

Linux中压缩、网络传输与系统监控工具的使用完整指南

《Linux中压缩、网络传输与系统监控工具的使用完整指南》在Linux系统管理中,压缩与传输工具是数据备份和远程协作的桥梁,而系统监控工具则是保障服务器稳定运行的眼睛,下面小编就来和大家详细介绍一下它... 目录引言一、压缩与解压:数据存储与传输的优化核心1. zip/unzip:通用压缩格式的便捷操作2.

Python实现对阿里云OSS对象存储的操作详解

《Python实现对阿里云OSS对象存储的操作详解》这篇文章主要为大家详细介绍了Python实现对阿里云OSS对象存储的操作相关知识,包括连接,上传,下载,列举等功能,感兴趣的小伙伴可以了解下... 目录一、直接使用代码二、详细使用1. 环境准备2. 初始化配置3. bucket配置创建4. 文件上传到os

Java内存分配与JVM参数详解(推荐)

《Java内存分配与JVM参数详解(推荐)》本文详解JVM内存结构与参数调整,涵盖堆分代、元空间、GC选择及优化策略,帮助开发者提升性能、避免内存泄漏,本文给大家介绍Java内存分配与JVM参数详解,... 目录引言JVM内存结构JVM参数概述堆内存分配年轻代与老年代调整堆内存大小调整年轻代与老年代比例元空

mysql表操作与查询功能详解

《mysql表操作与查询功能详解》本文系统讲解MySQL表操作与查询,涵盖创建、修改、复制表语法,基本查询结构及WHERE、GROUPBY等子句,本文结合实例代码给大家介绍的非常详细,感兴趣的朋友跟随... 目录01.表的操作1.1表操作概览1.2创建表1.3修改表1.4复制表02.基本查询操作2.1 SE

Go语言中nil判断的注意事项(最新推荐)

《Go语言中nil判断的注意事项(最新推荐)》本文给大家介绍Go语言中nil判断的注意事项,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.接口变量的特殊行为2.nil的合法类型3.nil值的实用行为4.自定义类型与nil5.反射判断nil6.函数返回的

Java中调用数据库存储过程的示例代码

《Java中调用数据库存储过程的示例代码》本文介绍Java通过JDBC调用数据库存储过程的方法,涵盖参数类型、执行步骤及数据库差异,需注意异常处理与资源管理,以优化性能并实现复杂业务逻辑,感兴趣的朋友... 目录一、存储过程概述二、Java调用存储过程的基本javascript步骤三、Java调用存储过程示