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

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念